Skip to content

Commit 2649ce4

Browse files
committed
Simplify create-docs script
Remove supertags, is_new param, fetching old docs. Add more comments. Remove commented out code.
1 parent 922e187 commit 2649ce4

File tree

1 file changed

+110
-168
lines changed

1 file changed

+110
-168
lines changed
Lines changed: 110 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/**
22
* This file is used to parse JSDoc for every tag and their regions
33
* and generate two artifacts out of it:
4-
* - tag docs for https://labelstud.io/tags/
4+
* - snippets for tag docs used by `insertmd` in https://labelstud.io/tags/
55
* generated docs are written to `outputDirArg` (1st arg)
6+
* only tag params, region params and example result jsons are included
67
* - schema.json — a dictionary for auto-complete in config editor
78
* generated file is written to `schemaJsonPath` (2nd arg or `SCHEMA_JSON_PATH` env var)
89
*
910
* Special new constructions:
1011
* - `@regions` to reference a Region tag(s) used by current tag
11-
* - `@subtag` to mark a tag used inside other tag (only Channel for now)
1212
*
1313
* Usage:
1414
* node scripts/create-docs.js [path/to/docs/dir] [path/to/schema.json]
@@ -24,14 +24,9 @@ const groups = [
2424
{ dir: "visual", title: "Visual & Experience", order: 501 },
2525
];
2626

27-
// tags that have subtags
28-
const supertags = ["TimeSeries"];
29-
3027
// glob pattern to check all possible extensions
3128
const EXT = "{js,jsx,ts,tsx}";
3229

33-
const currentTagsUrl = "https://api.github.com/repos/humansignal/label-studio/contents/docs/source/tags";
34-
3530
/**
3631
* Convert jsdoc parser type to simple actual type or list of possible values
3732
* @param {{ names: string[] }} type type from jsdoc
@@ -44,24 +39,6 @@ const attrType = ({ names } = {}) => {
4439
return names.length > 1 ? names : names[0];
4540
};
4641

47-
// header with tag info and autogenerated order
48-
// don't touch whitespaces
49-
const infoHeader = (name, group, isNew = false, meta = {}) =>
50-
[
51-
"---",
52-
...[
53-
`title: ${name}`,
54-
"type: tags",
55-
`order: ${groups.find((g) => g.dir === group).order++}`,
56-
isNew ? "is_new: t" : "",
57-
meta.title && `meta_title: ${meta.title}`,
58-
meta.description && `meta_description: ${meta.description}`,
59-
].filter(Boolean),
60-
"---",
61-
"",
62-
"",
63-
].join("\n");
64-
6542
const args = process.argv.slice(2);
6643
const outputDirArg = args[0] || `${__dirname}/../docs`;
6744
const outputDir = path.resolve(outputDirArg);
@@ -72,148 +49,113 @@ const schema = {};
7249

7350
fs.mkdirSync(outputDir, { recursive: true });
7451

75-
// get list of already exsting tags if possible to set `is_new` flag
76-
fetch(currentTagsUrl)
77-
.then((res) => (res.ok ? res.json() : null))
78-
.then((list) => list && list.map((file) => file.name.replace(/.md$/, "")))
79-
.catch(() => null)
80-
.then((tags) => {
81-
function processTemplate(t, dir, supertag) {
82-
// all tags are with this kind and leading capital letter
83-
if (t.kind !== "member" || !t.name.match(/^[A-Z]/)) return;
84-
// if (!supertag && t.customTags && t.customTags.find((desc) => desc.tag === "subtag")) return;
85-
const name = t.name.toLowerCase();
86-
// there are no new tags if we didn't get the list
87-
const isNew = tags ? !tags.includes(name) : false;
88-
const meta = t.customTags
89-
? Object.fromEntries(
90-
// convert @meta_* params into key-value hash
91-
t.customTags
92-
.filter((tag) => tag.tag.startsWith("meta_"))
93-
.map((tag) => [tag.tag.substr(5), tag.value]),
94-
)
95-
: {};
96-
const header = supertag ? `## ${t.name}\n\n` : infoHeader(t.name, dir, isNew, meta);
97-
98-
// generate tag details + all attributes
99-
schema[t.name] = {
100-
name: t.name,
101-
description: t.description,
102-
attrs: Object.fromEntries(
103-
t.params?.map((p) => [
104-
p.name,
105-
{
106-
name: p.name,
107-
description: p.description,
108-
type: attrType(p.type),
109-
required: !p.optional,
110-
default: p.defaultvalue,
111-
},
112-
]) ?? [],
113-
),
114-
};
115-
116-
// we can use comma-separated list of @regions used by tag
117-
const regions = t.customTags && t.customTags.find((desc) => desc.tag === "regions");
118-
// sample regions result and description
119-
let results = "";
120-
121-
if (regions) {
122-
for (const region of regions.value.split(/,\s*/)) {
123-
const files = path.resolve(`${__dirname}/../src/regions/${region}.${EXT}`);
124-
const regionsData = jsdoc2md.getTemplateDataSync({ files });
125-
// region descriptions named after region and defined as separate type:
126-
// @typedef {Object} AudioRegionResult
127-
const serializeData = regionsData.find((reg) => reg.name === `${region}Result`);
128-
129-
if (serializeData) {
130-
results = jsdoc2md
131-
.renderSync({ data: [serializeData], "example-lang": "json" })
132-
.split("\n")
133-
.slice(5) // remove first 5 lines with header
134-
.join("\n")
135-
.replace(/\*\*Example\*\*\s*\n/, "### Example JSON\n");
136-
results = `### Sample Results JSON\n${results}\n`;
137-
}
138-
}
139-
}
140-
141-
// remove all other @params we don't know how to use
142-
delete t.customTags;
143-
144-
let str = jsdoc2md
145-
.renderSync({ data: [t], "example-lang": "html" })
146-
// add header with info instead of header for github
147-
// don't add any header to subtags as they'll be inserted into supertag's doc
148-
// .replace(/^(.*?\n){3}/, header)
149-
// remove useless Kind: member
150-
.replace(/^.*?\*\*Kind\*\*.*?\n/ms, "### Parameters\n")
151-
.replace(/\*\*Example\*\*\s*\n.*/ms, results)
152-
// .replace(/\*\*Example\*\*\s*\n/g, "### Example\n")
153-
// move comments from examples to description
154-
// .replace(/```html[\n\s]*<!--[\n\s]*([\w\W]*?)[\n\s]*-->[\n\s]*/g, "\n$1\n\n```html\n")
155-
// change example language if it looks like JSON
156-
// .replace(/```html[\n\s]*([[{])/g, "```json\n$1")
157-
// normalize footnotes to be numbers (e.g. `[^FF_LSDV_0000]` => `[^1]`)
158-
.replace(
159-
/\[\^([^\]]+)\]/g,
160-
(() => {
161-
let footnoteLastIndex = 0;
162-
const footnoteIdToIdxMap = {};
163-
164-
return (match, footnoteId) => {
165-
const footnoteIdx = footnoteIdToIdxMap[footnoteId] || ++footnoteLastIndex;
166-
167-
footnoteIdToIdxMap[footnoteId] = footnoteIdx;
168-
return `[^${footnoteIdx}]`;
169-
};
170-
})(),
171-
)
172-
// force adding new lines before footnote definitions
173-
.replace(/(?<![\r\n])([\r\n])(\[\^[^\[]+\]:)/gm, "$1$1$2");
174-
175-
// if (supertags.includes(t.name)) {
176-
// console.log(`Fetching subtags of ${t.name}`);
177-
// const templates = jsdoc2md.getTemplateDataSync({ files: `${t.meta.path}/${t.name}/*.${EXT}` });
178-
// const subtags = templates
179-
// .map((t) => processTemplate(t, dir, t.name))
180-
// .filter(Boolean)
181-
// .join("\n\n");
182-
183-
// if (subtags) {
184-
// // insert before the first example or just at the end of doc
185-
// str = str.replace(/(### Example)|$/, `${subtags}\n$1`);
186-
// }
187-
// }
188-
189-
return str;
190-
}
191-
192-
for (const { dir, title, nested } of groups) {
193-
console.log(`## ${title}`);
194-
const prefix = `${__dirname}/../src/tags/${dir}`;
195-
const getTemplateDataByGlob = (glob) => jsdoc2md.getTemplateDataSync({ files: path.resolve(prefix + glob) });
196-
let templateData = getTemplateDataByGlob(`/*.${EXT}`);
197-
198-
if (nested) {
199-
templateData = templateData.concat(getTemplateDataByGlob(`/*/*.${EXT}`));
200-
}
201-
// tags inside nested dirs go after others, so we have to resort file list
202-
templateData.sort((a, b) => (a.name > b.name ? 1 : -1));
203-
for (const t of templateData) {
204-
const name = t.name.toLowerCase();
205-
const str = processTemplate(t, dir);
206-
207-
if (!str) continue;
208-
fs.writeFileSync(path.resolve(outputDir, `${name}.md`), str);
52+
/**
53+
* Generate tag details and schema for CodeMirror autocomplete for one tag
54+
* @param {Object} t — tag data from jsdoc2md
55+
* @returns {string} — tag details
56+
*/
57+
function processTemplate(t) {
58+
// all tags are with this kind and leading capital letter
59+
if (t.kind !== "member" || !t.name.match(/^[A-Z]/)) return;
60+
61+
// generate tag details + all attributes
62+
schema[t.name] = {
63+
name: t.name,
64+
description: t.description,
65+
attrs: Object.fromEntries(
66+
t.params?.map((p) => [
67+
p.name,
68+
{
69+
name: p.name,
70+
description: p.description,
71+
type: attrType(p.type),
72+
required: !p.optional,
73+
default: p.defaultvalue,
74+
},
75+
]) ?? [],
76+
),
77+
};
78+
79+
// we can use comma-separated list of @regions used by tag
80+
const regions = t.customTags && t.customTags.find((desc) => desc.tag === "regions");
81+
// sample regions result and description
82+
let results = "";
83+
84+
if (regions) {
85+
for (const region of regions.value.split(/,\s*/)) {
86+
const files = path.resolve(`${__dirname}/../src/regions/${region}.${EXT}`);
87+
const regionsData = jsdoc2md.getTemplateDataSync({ files });
88+
// region descriptions named after region and defined as separate type:
89+
// @typedef {Object} AudioRegionResult
90+
const serializeData = regionsData.find((reg) => reg.name === `${region}Result`);
91+
92+
if (serializeData) {
93+
results = jsdoc2md
94+
.renderSync({ data: [serializeData], "example-lang": "json" })
95+
.split("\n")
96+
.slice(5) // remove first 5 lines with header
97+
.join("\n")
98+
.replace(/\*\*Example\*\*\s*\n/, "### Example JSON\n");
99+
results = `### Result parameters\n${results}\n`;
209100
}
210101
}
211-
212-
if (schemaJsonPath) {
213-
// @todo we can't generate correct children for every tag for some reason
214-
// so for now we only specify children for the only root tag — View
215-
schema.View.children = Object.keys(schema).filter((name) => name !== "!top");
216-
fs.writeFileSync(schemaJsonPath, JSON.stringify(schema, null, 2));
217-
}
218-
})
219-
.catch(console.error);
102+
}
103+
104+
// remove all other @params we don't know how to use
105+
delete t.customTags;
106+
107+
let str = jsdoc2md
108+
.renderSync({ data: [t], "example-lang": "html" })
109+
// remove useless Kind: member
110+
.replace(/^.*?\*\*Kind\*\*.*?\n/ms, "### Parameters\n")
111+
.replace(/\*\*Example\*\*\s*\n.*/ms, results)
112+
// normalize footnotes to be numbers (e.g. `[^FF_LSDV_0000]` => `[^1]`)
113+
// @todo right now we don't have any footnotes, but code is helpful if we need them later
114+
.replace(
115+
/\[\^([^\]]+)\]/g,
116+
(() => {
117+
let footnoteLastIndex = 0;
118+
const footnoteIdToIdxMap = {};
119+
120+
return (_, footnoteId) => {
121+
const footnoteIdx = footnoteIdToIdxMap[footnoteId] || ++footnoteLastIndex;
122+
123+
footnoteIdToIdxMap[footnoteId] = footnoteIdx;
124+
return `[^${footnoteIdx}]`;
125+
};
126+
})(),
127+
)
128+
// force adding new lines before footnote definitions
129+
.replace(/(?<![\r\n])([\r\n])(\[\^[^\[]+\]:)/gm, "$1$1$2");
130+
131+
return str;
132+
}
133+
134+
////////////// PROCESS TAG DETAILS //////////////
135+
for (const { dir, title, nested } of groups) {
136+
console.log(`## ${title}`);
137+
const prefix = `${__dirname}/../src/tags/${dir}`;
138+
const getTemplateDataByGlob = (glob) => jsdoc2md.getTemplateDataSync({ files: path.resolve(prefix + glob) });
139+
let templateData = getTemplateDataByGlob(`/*.${EXT}`);
140+
141+
if (nested) {
142+
templateData = templateData.concat(getTemplateDataByGlob(`/*/*.${EXT}`));
143+
}
144+
// we have to reorder tags so they go alphabetically regardless of their dir
145+
templateData.sort((a, b) => (a.name > b.name ? 1 : -1));
146+
for (const t of templateData) {
147+
const name = t.name.toLowerCase();
148+
const str = processTemplate(t);
149+
150+
if (!str) continue;
151+
fs.writeFileSync(path.resolve(outputDir, `${name}.md`), str);
152+
}
153+
}
154+
155+
////////////// GENERATE SCHEMA //////////////
156+
if (schemaJsonPath) {
157+
// @todo we can't generate correct children for every tag for some reason
158+
// so for now we only specify children for the only root tag — View
159+
schema.View.children = Object.keys(schema).filter((name) => name !== "!top");
160+
fs.writeFileSync(schemaJsonPath, JSON.stringify(schema, null, 2));
161+
}

0 commit comments

Comments
 (0)