mirror of
https://github.com/youzan/vant.git
synced 2025-10-22 20:04:09 +00:00
refactor(markdown-vetur): support generate webstorm types (#5900)
This commit is contained in:
@@ -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;
|
||||
}
|
77
packages/vant-markdown-vetur/src/formatter.ts
Normal file
77
packages/vant-markdown-vetur/src/formatter.ts
Normal 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;
|
||||
}
|
@@ -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 };
|
||||
|
@@ -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;
|
63
packages/vant-markdown-vetur/src/type.ts
Normal file
63
packages/vant-markdown-vetur/src/type.ts
Normal 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;
|
||||
};
|
21
packages/vant-markdown-vetur/src/utils.ts
Normal file
21
packages/vant-markdown-vetur/src/utils.ts
Normal 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, '/');
|
||||
}
|
30
packages/vant-markdown-vetur/src/vetur.ts
Normal file
30
packages/vant-markdown-vetur/src/vetur.ts
Normal 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;
|
||||
}
|
19
packages/vant-markdown-vetur/src/web-types.ts
Normal file
19
packages/vant-markdown-vetur/src/web-types.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user