Skip to content

Commit b209024

Browse files
authored
feat: add IDE types tips generator (#3756)
* feat: add IDE types tips generator * fix: webtypes default lang en-US * chore: rename npmpublish allow files
1 parent 848d649 commit b209024

File tree

12 files changed

+471
-2
lines changed

12 files changed

+471
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,6 @@ package-lock.json
6969
list.txt
7070

7171
site/dev.js
72+
73+
# IDE 语法提示临时文件
74+
vetur/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fork github.com/youzan/vant packages/generator-types
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const path = require('path');
2+
const pkg = require('../../package.json');
3+
const { parseAndWrite } = require('./lib/index.js');
4+
const rootPath = path.resolve(__dirname, '../../');
5+
6+
try {
7+
parseAndWrite({
8+
version: pkg.version,
9+
name: 'types',
10+
path: path.resolve(rootPath, './v2-doc/src/docs'),
11+
// default match lang
12+
test: /en-US\.md/,
13+
outputDir: path.resolve(rootPath, './vetur'),
14+
tagPrefix: 'a-',
15+
});
16+
console.log('generator types success');
17+
} catch (e) {
18+
console.error('generator types error', e);
19+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* eslint-disable no-continue */
2+
import { Artical, Articals } from './parser';
3+
import { formatType, removeVersion, toKebabCase } from './utils';
4+
import { VueTag } from './type';
5+
6+
function getComponentName(name: string, tagPrefix: string) {
7+
if (name) {
8+
return tagPrefix + toKebabCase(name.split(' ')[0]);
9+
}
10+
return '';
11+
}
12+
13+
function parserProps(tag: VueTag, line: any) {
14+
const [name, desc, type, defaultVal] = line;
15+
if (
16+
type &&
17+
(type.includes('v-slot') ||
18+
type.includes('slot') ||
19+
type.includes('slots') ||
20+
type.includes('slot-scoped'))
21+
) {
22+
tag.slots!.push({
23+
name: removeVersion(name),
24+
description: desc,
25+
});
26+
}
27+
tag.attributes!.push({
28+
name: removeVersion(name),
29+
default: defaultVal,
30+
description: desc,
31+
value: {
32+
type: formatType(type || ''),
33+
kind: 'expression',
34+
},
35+
});
36+
}
37+
38+
export function formatter(articals: Articals, componentName: string, tagPrefix: string = '') {
39+
if (!articals.length) {
40+
return;
41+
}
42+
43+
const tags: VueTag[] = [];
44+
const tag: VueTag = {
45+
name: getComponentName(componentName, tagPrefix),
46+
slots: [],
47+
events: [],
48+
attributes: [],
49+
};
50+
tags.push(tag);
51+
52+
const tables = articals.filter(artical => artical.type === 'table');
53+
54+
tables.forEach(item => {
55+
const { table } = item;
56+
const prevIndex = articals.indexOf(item) - 1;
57+
const prevArtical = articals[prevIndex];
58+
59+
if (!prevArtical || !prevArtical.content || !table || !table.body) {
60+
return;
61+
}
62+
63+
const tableTitle = prevArtical.content;
64+
65+
if (tableTitle.includes('API')) {
66+
table.body.forEach(line => {
67+
parserProps(tag, line);
68+
});
69+
return;
70+
}
71+
72+
if (tableTitle.includes('events') && !tableTitle.includes(componentName)) {
73+
table.body.forEach(line => {
74+
const [name, desc] = line;
75+
tag.events!.push({
76+
name: removeVersion(name),
77+
description: desc,
78+
});
79+
});
80+
return;
81+
}
82+
83+
// 额外的子组件
84+
if (tableTitle.includes(componentName) && !tableTitle.includes('events')) {
85+
const childTag: VueTag = {
86+
name: getComponentName(tableTitle.replace('.', ''), tagPrefix),
87+
slots: [],
88+
events: [],
89+
attributes: [],
90+
};
91+
table.body.forEach(line => {
92+
parserProps(childTag, line);
93+
});
94+
tags.push(childTag);
95+
return;
96+
}
97+
// 额外的子组件事件
98+
if (tableTitle.includes(componentName) && tableTitle.includes('events')) {
99+
const childTagName = getComponentName(
100+
tableTitle.replace('.', '').replace('events', ''),
101+
tagPrefix,
102+
);
103+
const childTag: VueTag | undefined = tags.find(item => item.name === childTagName.trim());
104+
if (!childTag) {
105+
return;
106+
}
107+
table.body.forEach(line => {
108+
const [name, desc] = line;
109+
childTag.events!.push({
110+
name: removeVersion(name),
111+
description: desc,
112+
});
113+
});
114+
return;
115+
}
116+
});
117+
118+
return tags;
119+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import glob from 'fast-glob';
2+
import { join, dirname, basename } from 'path';
3+
import { mdParser } from './parser';
4+
import { formatter } from './formatter';
5+
import { genWebTypes } from './web-types';
6+
import { readFileSync, outputFileSync } from 'fs-extra';
7+
import { Options, VueTag } from './type';
8+
import { normalizePath, getComponentName } from './utils';
9+
import { genVeturTags, genVeturAttributes } from './vetur';
10+
11+
async function readMarkdown(options: Options) {
12+
// const mds = await glob(normalizePath(`${options.path}/**/*.md`))
13+
const mds = await glob(normalizePath(`${options.path}/**/*.md`));
14+
return mds
15+
.filter(md => options.test.test(md))
16+
.map(path => {
17+
const docPath = dirname(path);
18+
const componentName = docPath.substring(docPath.lastIndexOf('/') + 1);
19+
return {
20+
componentName: getComponentName(componentName || ''),
21+
md: readFileSync(path, 'utf-8'),
22+
};
23+
});
24+
}
25+
26+
export async function parseAndWrite(options: Options) {
27+
if (!options.outputDir) {
28+
throw new Error('outputDir can not be empty.');
29+
}
30+
31+
const docs = await readMarkdown(options);
32+
const datas = docs
33+
.map(doc => formatter(mdParser(doc.md), doc.componentName, options.tagPrefix))
34+
.filter(item => item) as VueTag[][];
35+
const tags: VueTag[] = [];
36+
datas.forEach(arr => {
37+
tags.push(...arr);
38+
});
39+
40+
const webTypes = genWebTypes(tags, options);
41+
const veturTags = genVeturTags(tags);
42+
const veturAttributes = genVeturAttributes(tags);
43+
44+
outputFileSync(join(options.outputDir, 'tags.json'), JSON.stringify(veturTags, null, 2));
45+
outputFileSync(
46+
join(options.outputDir, 'attributes.json'),
47+
JSON.stringify(veturAttributes, null, 2),
48+
);
49+
outputFileSync(join(options.outputDir, 'web-types.json'), JSON.stringify(webTypes, null, 2));
50+
}
51+
52+
export default { parseAndWrite };
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/* eslint-disable no-cond-assign */
2+
const TITLE_REG = /^(#+)\s+([^\n]*)/;
3+
const TABLE_REG = /^\|.+\r?\n\|\s*-+/;
4+
const TD_REG = /\s*`[^`]+`\s*|([^|`]+)/g;
5+
const TABLE_SPLIT_LINE_REG = /^\|\s*-/;
6+
7+
type TableContent = {
8+
head: string[];
9+
body: string[][];
10+
};
11+
12+
export type Artical = {
13+
type: string;
14+
content?: string;
15+
table?: TableContent;
16+
level?: number;
17+
};
18+
19+
export type Articals = Artical[];
20+
21+
function readLine(input: string) {
22+
const end = input.indexOf('\n');
23+
24+
return input.substr(0, end !== -1 ? end : input.length);
25+
}
26+
27+
function splitTableLine(line: string) {
28+
line = line.replace('\\|', 'JOIN');
29+
30+
const items = line.split('|').map(item => item.trim().replace('JOIN', '|'));
31+
32+
// remove pipe character on both sides
33+
items.pop();
34+
items.shift();
35+
36+
return items;
37+
}
38+
39+
function tableParse(input: string) {
40+
let start = 0;
41+
let isHead = true;
42+
43+
const end = input.length;
44+
const table: TableContent = {
45+
head: [],
46+
body: [],
47+
};
48+
49+
while (start < end) {
50+
const target = input.substr(start);
51+
const line = readLine(target);
52+
53+
if (!/^\|/.test(target)) {
54+
break;
55+
}
56+
57+
if (TABLE_SPLIT_LINE_REG.test(target)) {
58+
isHead = false;
59+
} else if (!isHead && line.includes('|')) {
60+
const matched = line.trim().match(TD_REG);
61+
62+
if (matched) {
63+
table.body.push(splitTableLine(line));
64+
}
65+
}
66+
67+
start += line.length + 1;
68+
}
69+
70+
return {
71+
table,
72+
usedLength: start,
73+
};
74+
}
75+
76+
export function mdParser(input: string): Articals {
77+
const artical = [];
78+
let start = 0;
79+
const end = input.length;
80+
// artical.push({
81+
// type: 'title',
82+
// content: title,
83+
// level: 0,
84+
// });
85+
86+
while (start < end) {
87+
const target = input.substr(start);
88+
89+
let match;
90+
if ((match = TITLE_REG.exec(target))) {
91+
artical.push({
92+
type: 'title',
93+
content: match[2].replace('\r', ''),
94+
level: match[1].length,
95+
});
96+
97+
start += match.index + match[0].length;
98+
} else if ((match = TABLE_REG.exec(target))) {
99+
const { table, usedLength } = tableParse(target.substr(match.index));
100+
artical.push({
101+
type: 'table',
102+
table,
103+
});
104+
105+
start += match.index + usedLength;
106+
} else {
107+
start += readLine(target).length + 1;
108+
}
109+
}
110+
111+
// artical[0].content = title
112+
return artical;
113+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { PathLike } from 'fs';
2+
3+
export type VueSlot = {
4+
name: string;
5+
description: string;
6+
};
7+
8+
export type VueEventArgument = {
9+
name: string;
10+
type: string;
11+
};
12+
13+
export type VueEvent = {
14+
name: string;
15+
description?: string;
16+
arguments?: VueEventArgument[];
17+
};
18+
19+
export type VueAttribute = {
20+
name: string;
21+
default: string;
22+
description: string;
23+
value: {
24+
kind: 'expression';
25+
type: string;
26+
};
27+
};
28+
29+
export type VueTag = {
30+
name: string;
31+
slots?: VueSlot[];
32+
events?: VueEvent[];
33+
attributes?: VueAttribute[];
34+
description?: string;
35+
};
36+
37+
export type VeturTag = {
38+
description?: string;
39+
attributes: string[];
40+
};
41+
42+
export type VeturTags = Record<string, VeturTag>;
43+
44+
export type VeturAttribute = {
45+
type: string;
46+
description: string;
47+
};
48+
49+
export type VeturAttributes = Record<string, VeturAttribute>;
50+
51+
export type VeturResult = {
52+
tags: VeturTags;
53+
attributes: VeturAttributes;
54+
};
55+
56+
export type Options = {
57+
name: string;
58+
path: PathLike;
59+
test: RegExp;
60+
version: string;
61+
outputDir?: string;
62+
tagPrefix?: string;
63+
};

0 commit comments

Comments
 (0)