refactor(markdown-vetur): support generate webstorm types (#5900)

This commit is contained in:
neverland
2020-03-24 20:43:28 +08:00
committed by GitHub
parent 7ae3a4a1b1
commit c74fbf58a4
12 changed files with 422 additions and 226 deletions

View File

@@ -1,89 +0,0 @@
/* eslint-disable no-continue */
import { Artical } from './md-parser';
const FLAG_REG = /(.*?)\s*(Props|Event)/i;
export type Tag = {
attributes: Record<string, Attribute>;
description: string;
defaults?: Array<string>;
subtags?: Array<string>;
};
export type Attribute = {
description: string;
type?: string;
options?: Array<string>;
};
function camelCaseToKebabCase(input: string): string {
return input.replace(
/[A-Z]/g,
(val, index) => (index === 0 ? '' : '-') + val.toLowerCase()
);
}
function removeVersionTag(str: string) {
return str.replace(/`(\w|\.)+`/g, '').trim();
}
function getDescription(td: string[], isProp: boolean) {
const desc = td[1] ? td[1].replace('<br>', '') : '';
const type = td[2] ? td[2].replace(/\*/g, '') : '';
const defaultVal = td[3] ? td[3].replace(/`/g, '') : '';
if (isProp) {
return `${desc}, 默认值: ${defaultVal}, 类型: ${type}`;
}
return desc;
}
export function codegen(artical: Artical) {
const tags: Record<string, Tag> = {};
let tagDescription = '';
for (let i = 0, len = artical.length; i < len; i++) {
const item = artical[i];
if (item.type === 'title' && item.level === 2) {
if (item.content) {
tagDescription = item.content;
}
} else if (item.type === 'table') {
const before = artical[i - 1];
if (!before || !before.content) {
continue;
}
const { table } = item;
const match = FLAG_REG.exec(before.content);
if (!match || !table) {
continue;
}
const key = camelCaseToKebabCase(match[1] || 'default');
const tag: Tag = tags[key] || {
description: tagDescription,
attributes: {},
};
tags[key] = tag;
const isProp = /Props/i.test(match[2]);
table.body.forEach(td => {
const name = removeVersionTag(td[0]);
const attr: Attribute = {
description: getDescription(td, isProp),
type: isProp ? td[2].replace(/`/g, '').toLowerCase() : 'event',
};
tag.attributes[name] = attr;
});
}
}
return tags;
}

View File

@@ -0,0 +1,77 @@
/* eslint-disable no-continue */
import { Artical, Articals } from './parser';
import { formatType, removeVersion, toKebabCase } from './utils';
import { VueTag } from './type';
function getComponentName(artical: Artical, tagPrefix: string) {
if (artical.content) {
return tagPrefix + toKebabCase(artical.content.split(' ')[0]);
}
return '';
}
export function formatter(articals: Articals, tagPrefix: string = '') {
if (!articals.length) {
return;
}
const tag: VueTag = {
name: getComponentName(articals[0], tagPrefix),
slots: [],
events: [],
attributes: [],
};
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('Props')) {
table.body.forEach(line => {
const [name, desc, type, defaultVal] = line;
tag.attributes!.push({
name: removeVersion(name),
default: defaultVal,
description: desc,
value: {
type: formatType(type),
kind: 'expression',
},
});
});
return;
}
if (tableTitle.includes('Events')) {
table.body.forEach(line => {
const [name, desc] = line;
tag.events!.push({
name: removeVersion(name),
description: desc,
});
});
return;
}
if (tableTitle.includes('Slots')) {
table.body.forEach(line => {
const [name, desc] = line;
tag.slots!.push({
name: removeVersion(name),
description: desc,
});
});
}
});
return tag;
}

View File

@@ -1,140 +1,46 @@
import glob from 'fast-glob';
import { join } from 'path';
import { mdParser } from './md-parser';
import { codegen, Tag, Attribute } from './codegen';
import {
PathLike,
statSync,
mkdirSync,
existsSync,
readdirSync,
readFileSync,
writeFileSync,
} from 'fs';
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 } from './utils';
import { genVeturTags, genVeturAttributes } from './vetur';
export function parseText(input: string) {
const ast = mdParser(input);
return codegen(ast);
async function readMarkdown(options: Options) {
const mds = await glob(normalizePath(`${options.path}/**/*.md`));
return mds
.filter(md => options.test.test(md))
.map(path => readFileSync(path, 'utf-8'));
}
export type Options = {
// 需要解析的文件夹路径
path: PathLike;
// 文件匹配正则
test: RegExp;
// 输出目录
outputDir?: string;
// 递归的目录最大深度
maxDeep?: number;
// 解析出来的组件名前缀
tagPrefix?: string;
};
export type ParseResult = {
tags: Record<
string,
{
description: string;
attributes: string[];
}
>;
attributes: Record<string, Attribute>;
};
const defaultOptions = {
maxDeep: Infinity,
tagPrefix: '',
};
export function parse(options: Options) {
options = {
...defaultOptions,
...options,
};
const result: ParseResult = {
tags: {},
attributes: {},
};
function putResult(componentName: string, component: Tag) {
componentName = options.tagPrefix + componentName;
const attributes = Object.keys(component.attributes);
const tag = {
description: component.description,
attributes,
};
result.tags[componentName] = tag;
attributes.forEach(key => {
result.attributes[`${componentName}/${key}`] = component.attributes[key];
});
}
function recursiveParse(options: Options, deep: number) {
if (options.maxDeep && deep > options.maxDeep) {
return;
}
deep++;
const files = readdirSync(options.path);
files.forEach(item => {
const currentPath = join(options.path.toString(), item);
const stats = statSync(currentPath);
if (stats.isDirectory()) {
recursiveParse(
{
...options,
path: currentPath,
},
deep
);
} else if (stats.isFile() && options.test.test(item)) {
const file = readFileSync(currentPath);
const tags = parseText(file.toString());
if (tags.default) {
// one tag
putResult(currentPath.split('/').slice(-2)[0], tags.default);
} else {
Object.keys(tags).forEach(key => {
putResult(key, tags[key]);
});
}
}
});
}
recursiveParse(options, 0);
return result;
}
export function parseAndWrite(options: Options) {
const { tags, attributes } = parse(options);
export async function parseAndWrite(options: Options) {
if (!options.outputDir) {
return;
throw new Error('outputDir can not be empty.');
}
const isExist = existsSync(options.outputDir);
if (!isExist) {
mkdirSync(options.outputDir);
}
const mds = await readMarkdown(options);
const datas = mds
.map(md => formatter(mdParser(md), options.tagPrefix))
.filter(item => !!item) as VueTag[];
writeFileSync(
const webTypes = genWebTypes(datas, options);
const veturTags = genVeturTags(datas);
const veturAttributes = genVeturAttributes(datas);
outputFileSync(
join(options.outputDir, 'tags.json'),
JSON.stringify(tags, null, 2)
JSON.stringify(veturTags, null, 2)
);
writeFileSync(
outputFileSync(
join(options.outputDir, 'attributes.json'),
JSON.stringify(attributes, null, 2)
JSON.stringify(veturAttributes, null, 2)
);
outputFileSync(
join(options.outputDir, 'web-types.json'),
JSON.stringify(webTypes, null, 2)
);
}
export default {
parse,
parseText,
parseAndWrite,
};
export default { parseAndWrite };

View File

@@ -4,19 +4,19 @@ const TABLE_REG = /^\|.+\n\|\s*-+/;
const TD_REG = /\s*`[^`]+`\s*|([^|`]+)/g;
const TABLE_SPLIT_LINE_REG = /^\|\s*-/;
interface TableContent {
head: Array<string>;
body: Array<Array<string>>;
}
type TableContent = {
head: string[];
body: string[][];
};
interface SimpleMdAst {
export type Artical = {
type: string;
content?: string;
table?: TableContent;
level?: number;
}
};
export interface Artical extends Array<SimpleMdAst> {}
export type Articals = Artical[];
function readLine(input: string) {
const end = input.indexOf('\n');
@@ -73,7 +73,7 @@ function tableParse(input: string) {
};
}
export function mdParser(input: string): Array<SimpleMdAst> {
export function mdParser(input: string): Articals {
const artical = [];
let start = 0;
const end = input.length;

View File

@@ -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;
};

View File

@@ -0,0 +1,21 @@
// 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
export function formatType(type: string) {
return type.replace(/\*/g, '');
}
export function normalizePath(path: string): string {
return path.replace(/\\/g, '/');
}

View File

@@ -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}, 默认值: ${attr.default}`,
};
});
}
});
return veturAttributes;
}

View File

@@ -0,0 +1,19 @@
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',
},
},
};
}