1
1
/**
2
2
* This file is used to parse JSDoc for every tag and their regions
3
3
* 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/
5
5
* generated docs are written to `outputDirArg` (1st arg)
6
+ * only tag params, region params and example result jsons are included
6
7
* - schema.json — a dictionary for auto-complete in config editor
7
8
* generated file is written to `schemaJsonPath` (2nd arg or `SCHEMA_JSON_PATH` env var)
8
9
*
9
10
* Special new constructions:
10
11
* - `@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)
12
12
*
13
13
* Usage:
14
14
* node scripts/create-docs.js [path/to/docs/dir] [path/to/schema.json]
@@ -24,14 +24,9 @@ const groups = [
24
24
{ dir : "visual" , title : "Visual & Experience" , order : 501 } ,
25
25
] ;
26
26
27
- // tags that have subtags
28
- const supertags = [ "TimeSeries" ] ;
29
-
30
27
// glob pattern to check all possible extensions
31
28
const EXT = "{js,jsx,ts,tsx}" ;
32
29
33
- const currentTagsUrl = "https://api.github.com/repos/humansignal/label-studio/contents/docs/source/tags" ;
34
-
35
30
/**
36
31
* Convert jsdoc parser type to simple actual type or list of possible values
37
32
* @param {{ names: string[] } } type type from jsdoc
@@ -44,24 +39,6 @@ const attrType = ({ names } = {}) => {
44
39
return names . length > 1 ? names : names [ 0 ] ;
45
40
} ;
46
41
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
-
65
42
const args = process . argv . slice ( 2 ) ;
66
43
const outputDirArg = args [ 0 ] || `${ __dirname } /../docs` ;
67
44
const outputDir = path . resolve ( outputDirArg ) ;
@@ -72,148 +49,113 @@ const schema = {};
72
49
73
50
fs . mkdirSync ( outputDir , { recursive : true } ) ;
74
51
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 ( / .m d $ / , "" ) ) )
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 ( / \* \* E x a m p l e \* \* \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 ( / ^ .* ?\* \* K i n d \* \* .* ?\n / ms, "### Parameters\n" )
151
- . replace ( / \* \* E x a m p l e \* \* \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 ( / \* \* E x a m p l e \* \* \s * \n / , "### Example JSON\n" ) ;
99
+ results = `### Result parameters\n${ results } \n` ;
209
100
}
210
101
}
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 ( / ^ .* ?\* \* K i n d \* \* .* ?\n / ms, "### Parameters\n" )
111
+ . replace ( / \* \* E x a m p l e \* \* \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