create-docs.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. /**
  2. * This file is used to parse JSDoc for every tag and their regions
  3. * and generate two artifacts out of it:
  4. * - snippets for tag docs used by `insertmd` in https://labelstud.io/tags/
  5. * generated docs are written to `outputDirArg` (1st arg)
  6. * only tag params, region params and example result jsons are included
  7. * - schema.json — a dictionary for auto-complete in config editor
  8. * generated file is written to `schemaJsonPath` (2nd arg or `SCHEMA_JSON_PATH` env var)
  9. *
  10. * Special new constructions:
  11. * - `@regions` to reference a Region tag(s) used by current tag
  12. *
  13. * Usage:
  14. * node scripts/create-docs.js [path/to/docs/dir] [path/to/schema.json]
  15. */
  16. const jsdoc2md = require("jsdoc-to-markdown");
  17. const fs = require("fs");
  18. const path = require("path");
  19. const groups = [
  20. { dir: "object", title: "Objects", order: 301, nested: true },
  21. { dir: "control", title: "Controls", order: 401, nested: true },
  22. { dir: "visual", title: "Visual & Experience", order: 501 },
  23. ];
  24. // glob pattern to check all possible extensions
  25. const EXT = "{js,jsx,ts,tsx}";
  26. /**
  27. * Convert jsdoc parser type to simple actual type or list of possible values
  28. * @param {{ names: string[] }} type type from jsdoc
  29. * @returns string[] | string
  30. */
  31. const attrType = ({ names } = {}) => {
  32. if (!names) return undefined;
  33. // boolean values are actually string literals "true" or "false" in config
  34. if (names[0] === "boolean") return ["true", "false"];
  35. return names.length > 1 ? names : names[0];
  36. };
  37. const args = process.argv.slice(2);
  38. const outputDirArg = args[0] || `${__dirname}/../docs`;
  39. const outputDir = path.resolve(outputDirArg);
  40. const schemaJsonPath = args[1] || process.env.SCHEMA_JSON_PATH;
  41. // schema for CodeMirror autocomplete
  42. const schema = {};
  43. fs.mkdirSync(outputDir, { recursive: true });
  44. /**
  45. * Generate tag details and schema for CodeMirror autocomplete for one tag
  46. * @param {Object} t — tag data from jsdoc2md
  47. * @returns {string} — tag details
  48. */
  49. function processTemplate(t) {
  50. // all tags are with this kind and leading capital letter
  51. if (t.kind !== "member" || !t.name.match(/^[A-Z]/)) return;
  52. // generate tag details + all attributes
  53. schema[t.name] = {
  54. name: t.name,
  55. description: t.description,
  56. attrs: Object.fromEntries(
  57. t.params?.map((p) => [
  58. p.name,
  59. {
  60. name: p.name,
  61. description: p.description,
  62. type: attrType(p.type),
  63. required: !p.optional,
  64. default: p.defaultvalue,
  65. },
  66. ]) ?? [],
  67. ),
  68. };
  69. // we can use comma-separated list of @regions used by tag
  70. const regions = t.customTags && t.customTags.find((desc) => desc.tag === "regions");
  71. // sample regions result and description
  72. let results = "";
  73. if (regions) {
  74. for (const region of regions.value.split(/,\s*/)) {
  75. const files = path.resolve(`${__dirname}/../src/regions/${region}.${EXT}`);
  76. try {
  77. const regionsData = jsdoc2md.getTemplateDataSync({ files });
  78. // region descriptions named after region and defined as separate type:
  79. // @typedef {Object} AudioRegionResult
  80. const serializeData = regionsData.find((reg) => reg.name === `${region}Result`);
  81. if (serializeData) {
  82. results = jsdoc2md
  83. .renderSync({ data: [serializeData], "example-lang": "json" })
  84. .split("\n")
  85. .slice(5) // remove first 5 lines with header
  86. .join("\n")
  87. .replace(/\*\*Example\*\*\s*\n/, "### Example JSON\n");
  88. results = `### Result parameters\n${results}\n`;
  89. }
  90. } catch (err) {
  91. console.error(err, files);
  92. }
  93. }
  94. }
  95. // remove all other @params we don't know how to use
  96. delete t.customTags;
  97. const str = jsdoc2md
  98. .renderSync({ data: [t], "example-lang": "html" })
  99. // remove useless Kind: member
  100. .replace(/^.*?\*\*Kind\*\*.*?\n/ms, "### Parameters\n")
  101. .replace(/\*\*Example\*\*\s*\n.*/ms, results)
  102. // normalize footnotes to be numbers (e.g. `[^FF_LSDV_0000]` => `[^1]`)
  103. // @todo right now we don't have any footnotes, but code is helpful if we need them later
  104. .replace(
  105. /\[\^([^\]]+)\]/g,
  106. (() => {
  107. let footnoteLastIndex = 0;
  108. const footnoteIdToIdxMap = {};
  109. return (_, footnoteId) => {
  110. const footnoteIdx = footnoteIdToIdxMap[footnoteId] || ++footnoteLastIndex;
  111. footnoteIdToIdxMap[footnoteId] = footnoteIdx;
  112. return `[^${footnoteIdx}]`;
  113. };
  114. })(),
  115. )
  116. // force adding new lines before footnote definitions
  117. .replace(/(?<![\r\n])([\r\n])(\[\^[^\[]+\]:)/gm, "$1$1$2");
  118. return str;
  119. }
  120. ////////////// PROCESS TAG DETAILS //////////////
  121. for (const { dir, title, nested } of groups) {
  122. console.log(`## ${title}`);
  123. const prefix = `${__dirname}/../src/tags/${dir}`;
  124. const getTemplateDataByGlob = (glob) => jsdoc2md.getTemplateDataSync({ files: path.resolve(prefix + glob) });
  125. let templateData = getTemplateDataByGlob(`/*.${EXT}`);
  126. if (nested) {
  127. templateData = templateData.concat(getTemplateDataByGlob(`/*/*.${EXT}`));
  128. }
  129. // we have to reorder tags so they go alphabetically regardless of their dir
  130. templateData.sort((a, b) => (a.name > b.name ? 1 : -1));
  131. for (const t of templateData) {
  132. const name = t.name.toLowerCase();
  133. const str = processTemplate(t);
  134. if (!str) continue;
  135. fs.writeFileSync(path.resolve(outputDir, `${name}.md`), str);
  136. }
  137. }
  138. ////////////// GENERATE SCHEMA //////////////
  139. if (schemaJsonPath) {
  140. // @todo we can't generate correct children for every tag for some reason
  141. // so for now we only specify children for the only root tag — View
  142. schema.View.children = Object.keys(schema).filter((name) => name !== "!top");
  143. fs.writeFileSync(schemaJsonPath, JSON.stringify(schema, null, 2));
  144. }