This repository was archived by the owner on Apr 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 320
/
Copy pathcreate-docs.js
187 lines (163 loc) · 6.98 KB
/
create-docs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
const jsdoc2md = require('jsdoc-to-markdown');
const fs = require('fs');
const path = require('path');
const groups = [
{ dir: 'object', title: 'Objects', order: 301, nested: true },
{ dir: 'control', title: 'Controls', order: 401, nested: true },
{ dir: 'visual', title: 'Visual & Experience', order: 501 },
];
const supertags = ['TimeSeries'];
const currentTagsUrl = 'https://api.github.com/repos/heartexlabs/label-studio/contents/docs/source/tags';
/**
* Conver jsdoc parser type to simple actual type or list of possible values
* @param {{ names: string[] }} type type from jsdoc
* @returns string[] | string
*/
const attrType = ({ names } = {}) => {
if (!names) return undefined;
// boolean values are actually string literals "true" or "false" in config
if (names[0] === 'boolean') return ['true', 'false'];
return names.length > 1 ? names : names[0];
};
// header with tag info and autogenerated order
// don't touch whitespaces
const infoHeader = (name, group, isNew = false, meta = {}) =>
[
'---',
...[
`title: ${name}`,
'type: tags',
`order: ${groups.find(g => g.dir === group).order++}`,
isNew ? 'is_new: t' : '',
meta.title && `meta_title: ${meta.title}`,
meta.description && `meta_description: ${meta.description}`,
].filter(Boolean),
'---',
'',
'',
].join('\n');
const outputDir = path.resolve(__dirname + '/../docs');
// schema for CodeMirror autocomplete
const schema = {};
fs.mkdirSync(outputDir, { recursive: true });
// get list of already exsting tags if possible to set `is_new` flag
fetch(currentTagsUrl)
.then(res => (res.ok ? res.json() : null))
.then(list => list && list.map(file => file.name.replace(/.md$/, '')))
.catch(() => null)
.then(tags => {
function processTemplate(t, dir, supertag) {
// all tags are with this kind and leading capital letter
if (t.kind !== 'member' || !t.name.match(/^[A-Z]/)) return;
if (!supertag && t.customTags && t.customTags.find(desc => desc.tag === 'subtag')) return;
const name = t.name.toLowerCase();
// there are no new tags if we didn't get the list
const isNew = tags ? !tags.includes(name) : false;
const meta = t.customTags
? Object.fromEntries(
// convert @meta_* params into key-value hash
t.customTags.filter(tag => tag.tag.startsWith('meta_')).map(tag => [tag.tag.substr(5), tag.value]),
)
: {};
const header = supertag ? `## ${t.name}\n\n` : infoHeader(t.name, dir, isNew, meta);
// generate tag details + all attributes
schema[t.name] = {
name: t.name,
description: t.description,
attrs: Object.fromEntries(t.params?.map(p => [
p.name,
{
name: p.name,
description: p.description,
type: attrType(p.type),
required: !p.optional,
default: p.defaultvalue,
},
]) ?? []),
};
// we can use comma-separated list of @regions used by tag
const regions = t.customTags && t.customTags.find(desc => desc.tag === 'regions');
// sample regions result and description
let results = '';
if (regions) {
for (const region of regions.value.split(/,\s*/)) {
const files = path.resolve(__dirname + '/../src/regions/' + region + '.js');
const regionsData = jsdoc2md.getTemplateDataSync({ files });
// region descriptions named after region and defined as separate type:
// @typedef {Object} AudioRegionResult
const serializeData = regionsData.find(reg => reg.name === region + 'Result');
if (serializeData) {
results = jsdoc2md
.renderSync({ 'data': [serializeData], 'example-lang': 'json' })
.split('\n')
.slice(5) // remove first 5 lines with header
.join('\n')
.replace(/\*\*Example\*\*\s*\n/, '### Example JSON\n');
results = `### Sample Results JSON\n${results}\n`;
}
}
}
// remove all other @params we don't know how to use
delete t.customTags;
let str = jsdoc2md
.renderSync({ 'data': [t], 'example-lang': 'html' })
// add header with info instead of header for github
// don't add any header to subtags as they'll be inserted into supertag's doc
.replace(/^(.*?\n){3}/, header)
// remove useless Kind: member
.replace(/\*\*Kind\*\*.*?\n/, '### Parameters\n')
.replace(/(\*\*Example\*\*\s*\n)/, results + '$1')
.replace(/\*\*Example\*\*\s*\n/g, '### Example\n')
// move comments from examples to description
.replace(/```html[\n\s]*<!--[\n\s]*([\w\W]*?)[\n\s]*-->[\n\s]*/g, '\n$1\n\n```html\n')
// change example language if it looks like JSON
.replace(/```html[\n\s]*([[{])/g, '```json\n$1')
// normalize footnotes to be numbers (e.g. `[^FF_LSDV_0000]` => `[^1]`)
.replace(/\[\^([^\]]+)\]/g, (()=>{
let footnoteLastIndex = 0;
const footnoteIdToIdxMap = {};
return (match, footnoteId) => {
const footnoteIdx = footnoteIdToIdxMap[footnoteId] || ++footnoteLastIndex;
footnoteIdToIdxMap[footnoteId] = footnoteIdx;
return `[^${footnoteIdx}]`;
};
})())
// force adding new lines before footnote definitions
.replace(/(?<![\r\n])([\r\n])(\[\^[^\[]+\]:)/gm, '$1$1$2');
if (supertags.includes(t.name)) {
console.log(`Fetching subtags of ${t.name}`);
const templates = jsdoc2md.getTemplateDataSync({ files: `${t.meta.path}/${t.name}/*.js` });
const subtags = templates
.map(t => processTemplate(t, dir, t.name))
.filter(Boolean)
.join('\n\n');
if (subtags) {
// insert before the first example or just at the end of doc
str = str.replace(/(### Example)|$/, `${subtags}\n$1`);
}
}
return str;
}
for (const { dir, title, nested } of groups) {
console.log('## ' + title);
const prefix = __dirname + '/../src/tags/' + dir;
const getTemplateDataByGlob = glob => jsdoc2md.getTemplateDataSync({ files: path.resolve(prefix + glob) });
let templateData = getTemplateDataByGlob('/*.js');
if (nested) {
templateData = templateData.concat(getTemplateDataByGlob('/*/*.js'));
}
// tags inside nested dirs go after others, so we have to resort file list
templateData.sort((a, b) => (a.name > b.name ? 1 : -1));
for (const t of templateData) {
const name = t.name.toLowerCase();
const str = processTemplate(t, dir);
if (!str) continue;
fs.writeFileSync(path.resolve(outputDir, `${name}.md`), str);
}
}
// for now only hardcoded list of all tags for View
schema.View.children = Object.keys(schema).filter(name => name !== '!top');
// @todo proper place
fs.writeFileSync(path.resolve(__dirname, 'tags-schema.json'), JSON.stringify(schema, null, 2));
})
.catch(console.error);