Browse Source
* feat: add IDE types tips generator * fix: webtypes default lang en-US * chore: rename npmpublish allow filespull/3197/head^2
言肆
4 years ago
committed by
GitHub
12 changed files with 471 additions and 2 deletions
@ -0,0 +1 @@
|
||||
fork github.com/youzan/vant packages/generator-types |
@ -0,0 +1,19 @@
|
||||
const path = require('path'); |
||||
const pkg = require('../../package.json'); |
||||
const { parseAndWrite } = require('./lib/index.js'); |
||||
const rootPath = path.resolve(__dirname, '../../'); |
||||
|
||||
try { |
||||
parseAndWrite({ |
||||
version: pkg.version, |
||||
name: 'types', |
||||
path: path.resolve(rootPath, './v2-doc/src/docs'), |
||||
// default match lang
|
||||
test: /en-US\.md/, |
||||
outputDir: path.resolve(rootPath, './vetur'), |
||||
tagPrefix: 'a-', |
||||
}); |
||||
console.log('generator types success'); |
||||
} catch (e) { |
||||
console.error('generator types error', e); |
||||
} |
@ -0,0 +1,119 @@
|
||||
/* eslint-disable no-continue */ |
||||
import { Artical, Articals } from './parser'; |
||||
import { formatType, removeVersion, toKebabCase } from './utils'; |
||||
import { VueTag } from './type'; |
||||
|
||||
function getComponentName(name: string, tagPrefix: string) { |
||||
if (name) { |
||||
return tagPrefix + toKebabCase(name.split(' ')[0]); |
||||
} |
||||
return ''; |
||||
} |
||||
|
||||
function parserProps(tag: VueTag, line: any) { |
||||
const [name, desc, type, defaultVal] = line; |
||||
if ( |
||||
type && |
||||
(type.includes('v-slot') || |
||||
type.includes('slot') || |
||||
type.includes('slots') || |
||||
type.includes('slot-scoped')) |
||||
) { |
||||
tag.slots!.push({ |
||||
name: removeVersion(name), |
||||
description: desc, |
||||
}); |
||||
} |
||||
tag.attributes!.push({ |
||||
name: removeVersion(name), |
||||
default: defaultVal, |
||||
description: desc, |
||||
value: { |
||||
type: formatType(type || ''), |
||||
kind: 'expression', |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
export function formatter(articals: Articals, componentName: string, tagPrefix: string = '') { |
||||
if (!articals.length) { |
||||
return; |
||||
} |
||||
|
||||
const tags: VueTag[] = []; |
||||
const tag: VueTag = { |
||||
name: getComponentName(componentName, tagPrefix), |
||||
slots: [], |
||||
events: [], |
||||
attributes: [], |
||||
}; |
||||
tags.push(tag); |
||||
|
||||
const tables = articals.filter(artical => artical.type === 'table'); |
||||
|
||||
tables.forEach(item => { |
||||
const { table } = item; |
||||
const prevIndex = articals.indexOf(item) - 1; |
||||
const prevArtical = articals[prevIndex]; |
||||
|
||||
if (!prevArtical || !prevArtical.content || !table || !table.body) { |
||||
return; |
||||
} |
||||
|
||||
const tableTitle = prevArtical.content; |
||||
|
||||
if (tableTitle.includes('API')) { |
||||
table.body.forEach(line => { |
||||
parserProps(tag, line); |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
if (tableTitle.includes('events') && !tableTitle.includes(componentName)) { |
||||
table.body.forEach(line => { |
||||
const [name, desc] = line; |
||||
tag.events!.push({ |
||||
name: removeVersion(name), |
||||
description: desc, |
||||
}); |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
// 额外的子组件
|
||||
if (tableTitle.includes(componentName) && !tableTitle.includes('events')) { |
||||
const childTag: VueTag = { |
||||
name: getComponentName(tableTitle.replace('.', ''), tagPrefix), |
||||
slots: [], |
||||
events: [], |
||||
attributes: [], |
||||
}; |
||||
table.body.forEach(line => { |
||||
parserProps(childTag, line); |
||||
}); |
||||
tags.push(childTag); |
||||
return; |
||||
} |
||||
// 额外的子组件事件
|
||||
if (tableTitle.includes(componentName) && tableTitle.includes('events')) { |
||||
const childTagName = getComponentName( |
||||
tableTitle.replace('.', '').replace('events', ''), |
||||
tagPrefix, |
||||
); |
||||
const childTag: VueTag | undefined = tags.find(item => item.name === childTagName.trim()); |
||||
if (!childTag) { |
||||
return; |
||||
} |
||||
table.body.forEach(line => { |
||||
const [name, desc] = line; |
||||
childTag.events!.push({ |
||||
name: removeVersion(name), |
||||
description: desc, |
||||
}); |
||||
}); |
||||
return; |
||||
} |
||||
}); |
||||
|
||||
return tags; |
||||
} |
@ -0,0 +1,52 @@
|
||||
import glob from 'fast-glob'; |
||||
import { join, dirname, basename } from 'path'; |
||||
import { mdParser } from './parser'; |
||||
import { formatter } from './formatter'; |
||||
import { genWebTypes } from './web-types'; |
||||
import { readFileSync, outputFileSync } from 'fs-extra'; |
||||
import { Options, VueTag } from './type'; |
||||
import { normalizePath, getComponentName } from './utils'; |
||||
import { genVeturTags, genVeturAttributes } from './vetur'; |
||||
|
||||
async function readMarkdown(options: Options) { |
||||
// const mds = await glob(normalizePath(`${options.path}/**/*.md`))
|
||||
const mds = await glob(normalizePath(`${options.path}/**/*.md`)); |
||||
return mds |
||||
.filter(md => options.test.test(md)) |
||||
.map(path => { |
||||
const docPath = dirname(path); |
||||
const componentName = docPath.substring(docPath.lastIndexOf('/') + 1); |
||||
return { |
||||
componentName: getComponentName(componentName || ''), |
||||
md: readFileSync(path, 'utf-8'), |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
export async function parseAndWrite(options: Options) { |
||||
if (!options.outputDir) { |
||||
throw new Error('outputDir can not be empty.'); |
||||
} |
||||
|
||||
const docs = await readMarkdown(options); |
||||
const datas = docs |
||||
.map(doc => formatter(mdParser(doc.md), doc.componentName, options.tagPrefix)) |
||||
.filter(item => item) as VueTag[][]; |
||||
const tags: VueTag[] = []; |
||||
datas.forEach(arr => { |
||||
tags.push(...arr); |
||||
}); |
||||
|
||||
const webTypes = genWebTypes(tags, options); |
||||
const veturTags = genVeturTags(tags); |
||||
const veturAttributes = genVeturAttributes(tags); |
||||
|
||||
outputFileSync(join(options.outputDir, 'tags.json'), JSON.stringify(veturTags, null, 2)); |
||||
outputFileSync( |
||||
join(options.outputDir, 'attributes.json'), |
||||
JSON.stringify(veturAttributes, null, 2), |
||||
); |
||||
outputFileSync(join(options.outputDir, 'web-types.json'), JSON.stringify(webTypes, null, 2)); |
||||
} |
||||
|
||||
export default { parseAndWrite }; |
@ -0,0 +1,113 @@
|
||||
/* eslint-disable no-cond-assign */ |
||||
const TITLE_REG = /^(#+)\s+([^\n]*)/; |
||||
const TABLE_REG = /^\|.+\r?\n\|\s*-+/; |
||||
const TD_REG = /\s*`[^`]+`\s*|([^|`]+)/g; |
||||
const TABLE_SPLIT_LINE_REG = /^\|\s*-/; |
||||
|
||||
type TableContent = { |
||||
head: string[]; |
||||
body: string[][]; |
||||
}; |
||||
|
||||
export type Artical = { |
||||
type: string; |
||||
content?: string; |
||||
table?: TableContent; |
||||
level?: number; |
||||
}; |
||||
|
||||
export type Articals = Artical[]; |
||||
|
||||
function readLine(input: string) { |
||||
const end = input.indexOf('\n'); |
||||
|
||||
return input.substr(0, end !== -1 ? end : input.length); |
||||
} |
||||
|
||||
function splitTableLine(line: string) { |
||||
line = line.replace('\\|', 'JOIN'); |
||||
|
||||
const items = line.split('|').map(item => item.trim().replace('JOIN', '|')); |
||||
|
||||
// remove pipe character on both sides
|
||||
items.pop(); |
||||
items.shift(); |
||||
|
||||
return items; |
||||
} |
||||
|
||||
function tableParse(input: string) { |
||||
let start = 0; |
||||
let isHead = true; |
||||
|
||||
const end = input.length; |
||||
const table: TableContent = { |
||||
head: [], |
||||
body: [], |
||||
}; |
||||
|
||||
while (start < end) { |
||||
const target = input.substr(start); |
||||
const line = readLine(target); |
||||
|
||||
if (!/^\|/.test(target)) { |
||||
break; |
||||
} |
||||
|
||||
if (TABLE_SPLIT_LINE_REG.test(target)) { |
||||
isHead = false; |
||||
} else if (!isHead && line.includes('|')) { |
||||
const matched = line.trim().match(TD_REG); |
||||
|
||||
if (matched) { |
||||
table.body.push(splitTableLine(line)); |
||||
} |
||||
} |
||||
|
||||
start += line.length + 1; |
||||
} |
||||
|
||||
return { |
||||
table, |
||||
usedLength: start, |
||||
}; |
||||
} |
||||
|
||||
export function mdParser(input: string): Articals { |
||||
const artical = []; |
||||
let start = 0; |
||||
const end = input.length; |
||||
// artical.push({
|
||||
// type: 'title',
|
||||
// content: title,
|
||||
// level: 0,
|
||||
// });
|
||||
|
||||
while (start < end) { |
||||
const target = input.substr(start); |
||||
|
||||
let match; |
||||
if ((match = TITLE_REG.exec(target))) { |
||||
artical.push({ |
||||
type: 'title', |
||||
content: match[2].replace('\r', ''), |
||||
level: match[1].length, |
||||
}); |
||||
|
||||
start += match.index + match[0].length; |
||||
} else if ((match = TABLE_REG.exec(target))) { |
||||
const { table, usedLength } = tableParse(target.substr(match.index)); |
||||
artical.push({ |
||||
type: 'table', |
||||
table, |
||||
}); |
||||
|
||||
start += match.index + usedLength; |
||||
} else { |
||||
start += readLine(target).length + 1; |
||||
} |
||||
} |
||||
|
||||
// artical[0].content = title
|
||||
return artical; |
||||
} |
@ -0,0 +1,63 @@
|
||||
import { PathLike } from 'fs'; |
||||
|
||||
export type VueSlot = { |
||||
name: string; |
||||
description: string; |
||||
}; |
||||
|
||||
export type VueEventArgument = { |
||||
name: string; |
||||
type: string; |
||||
}; |
||||
|
||||
export type VueEvent = { |
||||
name: string; |
||||
description?: string; |
||||
arguments?: VueEventArgument[]; |
||||
}; |
||||
|
||||
export type VueAttribute = { |
||||
name: string; |
||||
default: string; |
||||
description: string; |
||||
value: { |
||||
kind: 'expression'; |
||||
type: string; |
||||
}; |
||||
}; |
||||
|
||||
export type VueTag = { |
||||
name: string; |
||||
slots?: VueSlot[]; |
||||
events?: VueEvent[]; |
||||
attributes?: VueAttribute[]; |
||||
description?: string; |
||||
}; |
||||
|
||||
export type VeturTag = { |
||||
description?: string; |
||||
attributes: string[]; |
||||
}; |
||||
|
||||
export type VeturTags = Record<string, VeturTag>; |
||||
|
||||
export type VeturAttribute = { |
||||
type: string; |
||||
description: string; |
||||
}; |
||||
|
||||
export type VeturAttributes = Record<string, VeturAttribute>; |
||||
|
||||
export type VeturResult = { |
||||
tags: VeturTags; |
||||
attributes: VeturAttributes; |
||||
}; |
||||
|
||||
export type Options = { |
||||
name: string; |
||||
path: PathLike; |
||||
test: RegExp; |
||||
version: string; |
||||
outputDir?: string; |
||||
tagPrefix?: string; |
||||
}; |
@ -0,0 +1,30 @@
|
||||
// myName -> my-name
|
||||
export function toKebabCase(input: string): string { |
||||
return input.replace(/[A-Z]/g, (val, index) => (index === 0 ? '' : '-') + val.toLowerCase()); |
||||
} |
||||
|
||||
// name `v2.0.0` -> name
|
||||
export function removeVersion(str: string) { |
||||
return str.replace(/`(\w|\.)+`/g, '').trim(); |
||||
} |
||||
|
||||
// *boolean* -> boolean
|
||||
// _boolean_ -> boolean
|
||||
export function formatType(type: string) { |
||||
return type |
||||
.replace(/(^(\*|_))|((\*|_)$)/g, '') |
||||
.replace('\\', '') |
||||
.replace('\\', ''); |
||||
} |
||||
|
||||
export function getComponentName(name: string) { |
||||
let title = name |
||||
.split('-') |
||||
.map(it => it.substring(0, 1) + it.substring(1)) |
||||
.join(''); |
||||
return title.substring(0, 1).toUpperCase() + title.substring(1); |
||||
} |
||||
|
||||
export function normalizePath(path: string): string { |
||||
return path.replace(/\\/g, '/'); |
||||
} |
@ -0,0 +1,30 @@
|
||||
import { VueTag, VeturTags, VeturAttributes } from './type'; |
||||
|
||||
export function genVeturTags(tags: VueTag[]) { |
||||
const veturTags: VeturTags = {}; |
||||
|
||||
tags.forEach(tag => { |
||||
veturTags[tag.name] = { |
||||
attributes: tag.attributes ? tag.attributes.map(item => item.name) : [], |
||||
}; |
||||
}); |
||||
|
||||
return veturTags; |
||||
} |
||||
|
||||
export function genVeturAttributes(tags: VueTag[]) { |
||||
const veturAttributes: VeturAttributes = {}; |
||||
|
||||
tags.forEach(tag => { |
||||
if (tag.attributes) { |
||||
tag.attributes.forEach(attr => { |
||||
veturAttributes[`${tag.name}/${attr.name}`] = { |
||||
type: attr.value.type, |
||||
description: `${attr.description}, Default: ${attr.default}`, |
||||
}; |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
return veturAttributes; |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { VueTag, Options } from './type'; |
||||
|
||||
// create web-types.json to provide autocomplete in JetBrains IDEs
|
||||
export function genWebTypes(tags: VueTag[], options: Options) { |
||||
return { |
||||
$schema: 'https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json', |
||||
framework: 'vue', |
||||
name: options.name, |
||||
version: options.version, |
||||
contributions: { |
||||
html: { |
||||
tags, |
||||
attributes: [], |
||||
'types-syntax': 'typescript', |
||||
}, |
||||
}, |
||||
}; |
||||
} |
@ -0,0 +1,13 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2017", |
||||
"outDir": "./lib", |
||||
"module": "commonjs", |
||||
"strict": true, |
||||
"declaration": true, |
||||
"skipLibCheck": true, |
||||
"esModuleInterop": true, |
||||
"lib": ["esnext"] |
||||
}, |
||||
"include": ["src/**/*"] |
||||
} |
Loading…
Reference in new issue