@defold-typescript/library-types 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/api-doc/boom.json +3870 -0
  2. package/api-doc/bridge.json +10 -5
  3. package/api-doc/bzAnim.json +338 -0
  4. package/api-doc/decore.json +1 -1
  5. package/api-doc/defcon.json +150 -0
  6. package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
  7. package/api-doc/defsave.json +518 -0
  8. package/api-doc/deftest.json +741 -0
  9. package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
  10. package/api-doc/druid.json +90 -92
  11. package/api-doc/event.json +16 -16
  12. package/api-doc/gooey.json +2050 -0
  13. package/api-doc/in.accelerometer.json +75 -1
  14. package/api-doc/in.button.json +17 -10
  15. package/api-doc/in.cursor.json +26 -0
  16. package/api-doc/in.gesture.json +401 -6
  17. package/api-doc/in.keyboard.json +2 -0
  18. package/api-doc/in.mapper.json +12 -8
  19. package/api-doc/in.onscreen.json +61 -5
  20. package/api-doc/in.state.json +61 -13
  21. package/api-doc/in.textbox.json +23 -12
  22. package/api-doc/in.triggers.json +336 -0
  23. package/api-doc/lang.json +5 -5
  24. package/api-doc/log.json +1 -1
  25. package/api-doc/metrics.fps.json +61 -2
  26. package/api-doc/metrics.mem.json +61 -2
  27. package/api-doc/monarch.monarch.json +249 -7
  28. package/api-doc/monarch.transitions.easings.json +19 -26
  29. package/api-doc/monarch.transitions.gui.json +782 -79
  30. package/api-doc/nakama.engine.defold.json +31 -18
  31. package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
  32. package/api-doc/nakama.util.log.json +32 -4
  33. package/api-doc/narrator.json +1 -1
  34. package/api-doc/openapi/nakama.nakama.json +7680 -0
  35. package/api-doc/orthographic.camera.json +96 -98
  36. package/api-doc/orthographic.json +602 -0
  37. package/api-doc/panthera.json +1183 -0
  38. package/api-doc/{persist.persist.json → persist.json} +26 -1
  39. package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
  40. package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
  41. package/api-doc/richtext.color.json +77 -0
  42. package/api-doc/richtext.richtext.json +375 -5
  43. package/api-doc/saver.saver.json +2 -1
  44. package/api-doc/saver.storage.json +2 -1
  45. package/api-doc/{starly.starly.json → starly.json} +1 -1
  46. package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
  47. package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
  48. package/authored-targets.json +403 -0
  49. package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
  50. package/generated/bridge.d.ts +5 -5
  51. package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
  52. package/generated/decore.d.ts +1 -1
  53. package/generated/defcon.d.ts +43 -0
  54. package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
  55. package/generated/defsave.d.ts +144 -0
  56. package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
  57. package/generated/druid.d.ts +81 -79
  58. package/generated/event.d.ts +15 -15
  59. package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
  60. package/generated/in.onscreen.d.ts +5 -0
  61. package/generated/in.textbox.d.ts +4 -0
  62. package/generated/lang.d.ts +5 -5
  63. package/generated/log.d.ts +1 -1
  64. package/generated/metrics.fps.d.ts +19 -1
  65. package/generated/metrics.mem.d.ts +19 -1
  66. package/generated/monarch.monarch.d.ts +44 -1
  67. package/generated/monarch.transitions.easings.d.ts +0 -1
  68. package/generated/monarch.transitions.gui.d.ts +96 -12
  69. package/generated/nakama.d.ts +1158 -0
  70. package/generated/nakama.engine.defold.d.ts +3 -1
  71. package/generated/nakama.util.log.d.ts +6 -0
  72. package/generated/narrator.d.ts +1 -1
  73. package/generated/openapi/nakama.nakama.d.ts +884 -0
  74. package/generated/orthographic.camera.d.ts +24 -38
  75. package/generated/orthographic.d.ts +155 -0
  76. package/generated/panthera.d.ts +170 -0
  77. package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
  78. package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
  79. package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
  80. package/generated/richtext.color.d.ts +15 -0
  81. package/generated/saver.saver.d.ts +2 -0
  82. package/generated/saver.storage.d.ts +2 -0
  83. package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
  84. package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
  85. package/library-classification.json +0 -156
  86. package/library-targets.json +1 -218
  87. package/luals-targets.json +56 -3
  88. package/openapi-targets.json +17 -0
  89. package/package.json +7 -57
  90. package/scripts/__snapshots__/parse-luals.test.ts.snap +164 -10
  91. package/scripts/apply-luals-overrides.ts +39 -7
  92. package/scripts/authored-parity.ts +718 -0
  93. package/scripts/emit-library-dts.ts +94 -17
  94. package/scripts/extract-api-doc.ts +189 -38
  95. package/scripts/fidelity-floor.ts +178 -0
  96. package/scripts/import-upstream-docs.ts +125 -0
  97. package/scripts/lower-api-doc.ts +21 -3
  98. package/scripts/luals-fidelity.ts +16 -11
  99. package/scripts/map-luals-types.ts +52 -21
  100. package/scripts/parse-lua-surface.ts +187 -0
  101. package/scripts/parse-luals.ts +50 -4
  102. package/scripts/parse-markdown-api.ts +247 -0
  103. package/scripts/parse-openapi-api.ts +313 -0
  104. package/scripts/sync-authored-types.ts +249 -0
  105. package/scripts/sync-library-types.ts +99 -3
  106. package/scripts/sync-luals-types.ts +14 -1
  107. package/scripts/sync-markdown-types.ts +761 -0
  108. package/scripts/sync-openapi-types.ts +308 -0
  109. package/scripts/sync-script-api-types.ts +16 -17
  110. package/api-doc/boom.boom.json +0 -26
  111. package/api-doc/bzAnim.bzLibrary.json +0 -156
  112. package/api-doc/defcon.console.json +0 -109
  113. package/api-doc/defsave.defsave.json +0 -175
  114. package/api-doc/deftest.deftest.json +0 -71
  115. package/api-doc/gooey.gooey.json +0 -963
  116. package/generated/defcon.console.d.ts +0 -24
  117. package/generated/defsave.defsave.d.ts +0 -31
  118. package/generated/nakama.nakama.d.ts +0 -594
  119. /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
  120. /package/generated/{starly.starly.d.ts → starly.d.ts} +0 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * A line-oriented reader for the *public surface* of a plain-Lua module — the
3
+ * member names and parameter names a `local M = {} … return M` file declares.
4
+ *
5
+ * `sync-authored-types.ts` gates the authored/forked lane on a forked-vs-generated
6
+ * identity diff and records that there is no coverage comparison against a primary
7
+ * source. That holds for *types*: upstream Lua declares none. It does not hold for
8
+ * *surface*: names and arity are right there in the source, so a fork that drops a
9
+ * member or a trailing parameter is measurable. This module reads that side; the
10
+ * comparison lives in `authored-parity.ts`.
11
+ *
12
+ * The corpus is a handful of pinned files, so line scanning is what it needs — a
13
+ * Lua grammar would buy nothing here. Two rules keep it honest:
14
+ *
15
+ * - **Column 0 only.** A definition is a member when it starts the line. This is
16
+ * what keeps `nakama/util/log.lua`'s `M.log = noop` — reassigned inside three
17
+ * different function bodies — out of the surface, and it is why the reader never
18
+ * needs to know where a block ends.
19
+ * - **Loud failure over silent undercount.** A missing `return <name>` or a
20
+ * parameter list that does not close on its own line throws, as does a
21
+ * `return setmetatable(M, …)` whose metatable carries `__index` or is named by a
22
+ * variable whose keys this reader cannot see — either could add members no column-0
23
+ * scan will find. The whole point of the instrument is that a dropped member is
24
+ * visible; a parser that quietly skips what it cannot read would inflate every
25
+ * coverage number it feeds.
26
+ */
27
+
28
+ export interface LuaMember {
29
+ name: string;
30
+ /** Absent for a non-callable field (`M.SOME_CONSTANT = "X"`), so a field can
31
+ * never be confused with a zero-arity function. */
32
+ params?: string[];
33
+ /** True when the definition ends in `...`, which is not a named parameter. */
34
+ varargs: boolean;
35
+ /** The `---` LuaDoc block immediately above the definition, comment markers
36
+ * stripped, one interior blank line crossed; empty when the block is absent or
37
+ * opens with a plain `--`. */
38
+ doc: string;
39
+ /** True when a comment block *was* there and the `---` rule declined it, which `doc`
40
+ * alone cannot say — an empty `doc` otherwise means upstream documented nothing. */
41
+ refusedDoc: boolean;
42
+ }
43
+
44
+ export interface LuaSurface {
45
+ moduleLocal: string;
46
+ members: LuaMember[];
47
+ }
48
+
49
+ const RETURN_LINE = /^return\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?\s*$/;
50
+ const RETURN_SETMETATABLE = /^return\s+setmetatable\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,/;
51
+ const IDENTIFIER = "[A-Za-z_][A-Za-z0-9_]*";
52
+
53
+ /** The metatable argument of a `return setmetatable(<name>, …)`, refused unless its
54
+ * keys are readable and none of them delegates. `text` runs from the comma to the end
55
+ * of the source rather than to the matching brace: over-reading can only refuse a file
56
+ * loudly, where under-reading would accept a delegating module and report its short
57
+ * surface as complete. */
58
+ function assertNoDelegation(text: string): void {
59
+ if (text.trimStart().startsWith("{") === false) {
60
+ throw new Error(
61
+ "lua surface: the returned `setmetatable` is given a metatable this reader cannot see the keys of, so a delegated member would be invisible — inline the table literal or measure the module another way.",
62
+ );
63
+ }
64
+ if (text.includes("__index")) {
65
+ throw new Error(
66
+ "lua surface: the returned metatable carries `__index`, so members reached through it would be invisible to a column-0 scan and the surface would read as complete while short.",
67
+ );
68
+ }
69
+ }
70
+
71
+ function resolveModuleLocal(lines: string[]): string {
72
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
73
+ const line = lines[index] as string;
74
+ const bare = RETURN_LINE.exec(line);
75
+ if (bare) return bare[1] as string;
76
+ const wrapped = RETURN_SETMETATABLE.exec(line);
77
+ if (wrapped) {
78
+ assertNoDelegation(
79
+ [line.slice((wrapped[0] as string).length), ...lines.slice(index + 1)].join("\n"),
80
+ );
81
+ return wrapped[1] as string;
82
+ }
83
+ }
84
+ throw new Error(
85
+ "lua surface: the source has no trailing `return <name>`, so the module-local name cannot be derived.",
86
+ );
87
+ }
88
+
89
+ /** The parameter names between the parentheses opened at `open` on `line`, with a
90
+ * `...` tail reported separately. Throws when the list does not close on the same
91
+ * line rather than dropping the member. */
92
+ function readParams(
93
+ line: string,
94
+ open: number,
95
+ lineNumber: number,
96
+ ): { params: string[]; varargs: boolean } {
97
+ const close = line.indexOf(")", open);
98
+ if (close === -1) {
99
+ throw new Error(
100
+ `lua surface: line ${lineNumber} opens a parameter list that does not close on the same line — ${line.trim()}`,
101
+ );
102
+ }
103
+ const raw = line
104
+ .slice(open + 1, close)
105
+ .split(",")
106
+ .map((part) => part.trim())
107
+ .filter((part) => part !== "");
108
+ return { params: raw.filter((part) => part !== "..."), varargs: raw.includes("...") };
109
+ }
110
+
111
+ /** The contiguous run of comment lines ending at `index - 1`, in source order. */
112
+ function readSegment(lines: string[], index: number): string[] {
113
+ const segment: string[] = [];
114
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
115
+ const line = (lines[cursor] as string).trim();
116
+ if (!line.startsWith("--") || line.startsWith("--[[")) break;
117
+ segment.unshift(line);
118
+ }
119
+ return segment;
120
+ }
121
+
122
+ /** The contiguous comment block ending at `index - 1`, kept only when it opens
123
+ * with `---`. LuaDoc in this corpus is a `---` summary followed by plain `--`
124
+ * `@param`/`@return` lines, so the marker on the *first* line is what decides.
125
+ *
126
+ * A single blank line inside the run is crossed, and only when the segment
127
+ * attached to the definition opens no block of its own — upstream wraps a long
128
+ * `@param` list around a blank, which would otherwise truncate the block to its
129
+ * tag half and lose the summary above. A segment carrying its own `---` is never
130
+ * absorbed into the one below it, so a blank-separated section header stays out. */
131
+ function readDoc(lines: string[], index: number): Pick<LuaMember, "doc" | "refusedDoc"> {
132
+ const attached = readSegment(lines, index);
133
+ if (attached.length === 0) return { doc: "", refusedDoc: false };
134
+ const above = index - attached.length - 1;
135
+ const block =
136
+ attached.some((line) => line.startsWith("---")) ||
137
+ above < 0 ||
138
+ (lines[above] as string).trim() !== ""
139
+ ? attached
140
+ : [...readSegment(lines, above), ...attached];
141
+ if (!(block[0] as string).startsWith("---")) return { doc: "", refusedDoc: true };
142
+ const doc = block
143
+ .map((line) => line.replace(/^-+\s?/, "").trimEnd())
144
+ .join("\n")
145
+ .trim();
146
+ return { doc, refusedDoc: false };
147
+ }
148
+
149
+ /**
150
+ * The public surface of a Lua module source: the module-local name it returns and
151
+ * every member assigned to it at column 0, in source order. A name defined more
152
+ * than once keeps its first position and its last definition, matching Lua.
153
+ */
154
+ export function parseLuaSurface(source: string): LuaSurface {
155
+ const lines = source.split("\n");
156
+ const moduleLocal = resolveModuleLocal(lines);
157
+ const definition = new RegExp(`^function\\s+${moduleLocal}\\.(${IDENTIFIER})\\s*\\(`);
158
+ const assignment = new RegExp(`^${moduleLocal}\\.(${IDENTIFIER})\\s*=\\s*(.*)$`);
159
+ const assignedFunction = /^function\s*\(/;
160
+
161
+ const members = new Map<string, LuaMember>();
162
+ const record = (member: LuaMember): void => {
163
+ members.set(member.name, member);
164
+ };
165
+
166
+ for (const [index, line] of lines.entries()) {
167
+ const defined = definition.exec(line);
168
+ if (defined) {
169
+ const { params, varargs } = readParams(line, defined[0].length - 1, index + 1);
170
+ record({ name: defined[1] as string, params, varargs, ...readDoc(lines, index) });
171
+ continue;
172
+ }
173
+ const assigned = assignment.exec(line);
174
+ if (!assigned) continue;
175
+ const name = assigned[1] as string;
176
+ const rhs = assigned[2] as string;
177
+ if (!assignedFunction.test(rhs)) {
178
+ record({ name, varargs: false, ...readDoc(lines, index) });
179
+ continue;
180
+ }
181
+ const open = line.indexOf("(", line.length - rhs.length);
182
+ const { params, varargs } = readParams(line, open, index + 1);
183
+ record({ name, params, varargs, ...readDoc(lines, index) });
184
+ }
185
+
186
+ return { moduleLocal, members: [...members.values()] };
187
+ }
@@ -35,6 +35,7 @@ export interface LibraryInterface {
35
35
  // so an interface without overloads carries no key. The emitter renders each as an
36
36
  // interface call signature; the mapper maps the token to a `(params): ret` form.
37
37
  overloads?: LibraryOverload[];
38
+ deprecated?: string;
38
39
  }
39
40
 
40
41
  export interface LibraryOverload {
@@ -53,6 +54,10 @@ export interface LibraryMethod {
53
54
  // unmarked method carries no key. `local` has no `@field` analogue (LuaLS field
54
55
  // scope has no `local`), so a method's visibility widens the field set with it.
55
56
  visibility?: LibraryMethodVisibility;
57
+ // A `---@deprecated` on the pending block. Present exactly when the tag is, with
58
+ // `""` for the bare form — absence of the key is the sole encoding of "not
59
+ // deprecated", so a bare tag must never be collapsed away by a truthiness guard.
60
+ deprecated?: string;
56
61
  }
57
62
 
58
63
  export interface LibraryParam {
@@ -89,6 +94,7 @@ export interface LibraryAlias {
89
94
  name: string;
90
95
  types: string[];
91
96
  doc: string;
97
+ deprecated?: string;
92
98
  }
93
99
 
94
100
  interface Pending {
@@ -98,6 +104,7 @@ interface Pending {
98
104
  generics: LibraryGeneric[];
99
105
  overloads: LibraryOverload[];
100
106
  visibility?: LibraryMethodVisibility;
107
+ deprecated?: string;
101
108
  }
102
109
 
103
110
  const emptyPending = (): Pending => ({
@@ -316,7 +323,14 @@ function parseParam(rest: string): LibraryParam {
316
323
  return param;
317
324
  }
318
325
 
319
- function parseReturn(rest: string): LibraryParam {
326
+ /**
327
+ * A single `---@return` line may declare several values (`---@return number, number`),
328
+ * which `readTypeToken`'s comma continuation hands back as one token. Split it on
329
+ * top-level commas into one entry per value; LuaLS attaches the optional trailing name
330
+ * and description to the whole line, so only the last segment carries them. The
331
+ * emitter's `renderReturn` already lowers a multi-entry `returns` to `LuaMultiReturn`.
332
+ */
333
+ function parseReturn(rest: string): LibraryParam[] {
320
334
  const { type, rest: afterType } = readTypeToken(rest);
321
335
  const spaceAt = afterType.search(/\s/);
322
336
  const head = spaceAt === -1 ? afterType : afterType.slice(0, spaceAt);
@@ -326,7 +340,22 @@ function parseReturn(rest: string): LibraryParam {
326
340
  name = head;
327
341
  doc = spaceAt === -1 ? "" : afterType.slice(spaceAt).trim();
328
342
  }
329
- return { name, types: type ? [type] : [], doc, isOptional: false, isVararg: false };
343
+ const segments = (type ? splitTopLevel(type, ",") : [""])
344
+ .map((segment) => segment.trim())
345
+ .filter((segment) => segment !== "");
346
+ if (segments.length === 0) {
347
+ return [{ name, types: [], doc, isOptional: false, isVararg: false }];
348
+ }
349
+ return segments.map((segment, index) => {
350
+ const isLast = index === segments.length - 1;
351
+ return {
352
+ name: isLast ? name : "",
353
+ types: [segment],
354
+ doc: isLast ? doc : "",
355
+ isOptional: false,
356
+ isVararg: false,
357
+ };
358
+ });
330
359
  }
331
360
 
332
361
  const VISIBILITY_KEYWORDS = new Set<LibraryFieldVisibility>([
@@ -546,6 +575,7 @@ export function parseLualsSource(source: string): LibraryModel {
546
575
  params: pending.params,
547
576
  returns: pending.returns,
548
577
  ...(pending.visibility ? { visibility: pending.visibility } : {}),
578
+ ...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
549
579
  });
550
580
 
551
581
  // Interpret one indented line while a function-local `---@class` block is open. Only
@@ -646,6 +676,7 @@ export function parseLualsSource(source: string): LibraryModel {
646
676
  if (pending.doc.length > 0 && iface.brief === "") iface.brief = pending.doc.join("\n");
647
677
  if (pending.generics.length > 0) iface.generics = pending.generics;
648
678
  if (pending.overloads.length > 0) iface.overloads = pending.overloads;
679
+ if (pending.deprecated !== undefined) iface.deprecated = pending.deprecated;
649
680
  openClass = iface;
650
681
  lastOpenedClass = head.name;
651
682
  pending = emptyPending();
@@ -664,7 +695,7 @@ export function parseLualsSource(source: string): LibraryModel {
664
695
  break;
665
696
  }
666
697
  case "return": {
667
- pending.returns.push(parseReturn(rest));
698
+ pending.returns.push(...parseReturn(rest));
668
699
  break;
669
700
  }
670
701
  case "generic": {
@@ -684,7 +715,12 @@ export function parseLualsSource(source: string): LibraryModel {
684
715
  const spaceAt = rest.search(/\s/);
685
716
  const name = spaceAt === -1 ? rest : rest.slice(0, spaceAt);
686
717
  const expr = spaceAt === -1 ? "" : rest.slice(spaceAt).trim();
687
- aliases.push({ name, types: expr ? [expr] : [], doc: pending.doc.join("\n") });
718
+ aliases.push({
719
+ name,
720
+ types: expr ? [expr] : [],
721
+ doc: pending.doc.join("\n"),
722
+ ...(pending.deprecated !== undefined ? { deprecated: pending.deprecated } : {}),
723
+ });
688
724
  pending = emptyPending();
689
725
  break;
690
726
  }
@@ -698,6 +734,12 @@ export function parseLualsSource(source: string): LibraryModel {
698
734
  pending.visibility = tag as LibraryMethodVisibility;
699
735
  break;
700
736
  }
737
+ case "deprecated": {
738
+ // `rest` is already trimmed and is `""` for the bare form, which is the
739
+ // only form the fixtures actually ship; the text form is carried too.
740
+ pending.deprecated = rest;
741
+ break;
742
+ }
701
743
  default:
702
744
  // @cast, @type, @diagnostic, ... — outside the Druid subset; recognized as a
703
745
  // tag and skipped, never treated as doc.
@@ -793,6 +835,7 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
793
835
  ...(iface.overloads && iface.overloads.length > 0
794
836
  ? { overloads: [...iface.overloads] }
795
837
  : {}),
838
+ ...(iface.deprecated !== undefined ? { deprecated: iface.deprecated } : {}),
796
839
  };
797
840
  byName.set(iface.name, copy);
798
841
  interfaces.push(copy);
@@ -801,6 +844,9 @@ export function mergeLibraryModels(models: LibraryModel[]): LibraryModel {
801
844
  existing.fields.push(...iface.fields);
802
845
  existing.methods.push(...iface.methods);
803
846
  if (!existing.extends && iface.extends) existing.extends = iface.extends;
847
+ if (existing.deprecated === undefined && iface.deprecated !== undefined) {
848
+ existing.deprecated = iface.deprecated;
849
+ }
804
850
  if (existing.brief === "" && iface.brief !== "") existing.brief = iface.brief;
805
851
  if (existing.generics.length === 0 && iface.generics.length > 0) {
806
852
  existing.generics = [...iface.generics];
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The markdown ingestion front-end's parser: a third `library-types` corpus
3
+ * source beside the LuaLS annotations (`parse-luals.ts`) and the typed
4
+ * `.script_api` (`scriptApiToFixtureJson`). It reads a library's README/`.md`
5
+ * prose API — the only machine-readable type source Bucket-C libraries ship —
6
+ * and produces the same ref-doc `doc` shape those two front-ends feed the shared
7
+ * emitter (`generateModuleDeclaration`), so the markdown path reuses the exact
8
+ * emit + fidelity machinery.
9
+ *
10
+ * Scope is a **flat signature surface**, not deep prose (a PRD non-goal): the
11
+ * parser lifts one element per `##`- or `###`-level `<receiver>.<fn>(<args>)` API
12
+ * header and its `**PARAMETERS**` / `**RETURN**` bullet lists. Both levels are
13
+ * accepted because the corpus is split on the convention (defold-orthographic and
14
+ * defold-input write `###`; monarch's `README_API.md` writes `##`); h1 and h4 stay
15
+ * outside the range, since at those levels a dotted-call-shaped line is document
16
+ * structure rather than a signature. At either level the receiver may be preceded
17
+ * by a literal `function ` declaration keyword (rendy writes 9 of its 11 headings
18
+ * that way); no other prefix is accepted, so prose such as `### see mod.fn()` is
19
+ * still not a signature. Header-only message sections (`<verb>` with
20
+ * no dotted receiver or parens) and nested option-table bullets are ignored. A
21
+ * signature row that names a parameter but gives it no `(type)` loud-fails rather
22
+ * than silently emitting an untyped `any`.
23
+ */
24
+
25
+ /** A single ref-doc parameter or return slot. `is_optional` mirrors the
26
+ * consumer contract's string flag (`"True"`); it is present only when set. */
27
+ export interface MarkdownParam {
28
+ name: string;
29
+ doc: string;
30
+ types: string[];
31
+ is_optional?: "True";
32
+ }
33
+
34
+ export interface MarkdownElement {
35
+ type: "FUNCTION";
36
+ name: string;
37
+ description: string;
38
+ parameters: MarkdownParam[];
39
+ returnvalues: MarkdownParam[];
40
+ }
41
+
42
+ /** The ref-doc `doc` shape `generateModuleDeclaration` consumes. `info.namespace`
43
+ * carries the README's own module prefix; the front-end retargets it to the
44
+ * pinned namespace before emitting. */
45
+ export interface MarkdownDoc {
46
+ info: { namespace: string; brief: string; description: string };
47
+ elements: MarkdownElement[];
48
+ }
49
+
50
+ // Only the literal `function` keyword is accepted before the receiver — a general
51
+ // `\w+\s+` prefix would make prose like `### see rendy.set(...)` read as a signature.
52
+ const HEADER = /^#{2,3}\s+(?:function\s+)?([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)\((.*)\)\s*$/;
53
+ const PARAM_MARKER = /^\*\*PARAM(?:ETER|ETERS)?\*\*\s*$/;
54
+ const RETURN_MARKER = /^\*\*RETURNS?\*\*\s*$/;
55
+ // A bullet with a backticked name and a required `(type)` group. A named bullet
56
+ // missing the `(type)` group is an unresolvable row (see `parseSlot`).
57
+ const TYPED_BULLET = /^\*\s+`([^`]+)`\s*\(([^)]*)\)\s*-?\s*(.*)$/;
58
+ const NAMED_BULLET = /^\*\s+`([^`]+)`/;
59
+
60
+ /** Bracketed header arguments are optional; collect their bare names. A bracket
61
+ * may be escaped (`duration \[, scaler]` — a README authoring artifact so the
62
+ * upstream renderer does not read `[, scaler]` as a link), may span a comma
63
+ * (`data [, overwrite]`), and may cover several arguments at once (`[b, c]`),
64
+ * in which case every argument inside it is optional. Unlike `splitTypes`, a
65
+ * comma at any depth ends the current argument: in an argument list a comma
66
+ * always separates arguments, whether or not a bracket group spans it. */
67
+ function bracketedArgs(argList: string): Set<string> {
68
+ const optional = new Set<string>();
69
+ let depth = 0;
70
+ let current = "";
71
+ let bracketed = false;
72
+
73
+ const flush = () => {
74
+ const arg = current.trim();
75
+ if (arg.length > 0 && bracketed) optional.add(arg);
76
+ current = "";
77
+ bracketed = false;
78
+ };
79
+
80
+ for (const ch of argList.replace(/\\\[/g, "[").replace(/\\\]/g, "]")) {
81
+ if (ch === "[") {
82
+ depth++;
83
+ continue;
84
+ }
85
+ if (ch === "]" && depth > 0) {
86
+ depth--;
87
+ continue;
88
+ }
89
+ if (ch === ",") {
90
+ flush();
91
+ continue;
92
+ }
93
+ if (depth > 0 && ch.trim().length > 0) bracketed = true;
94
+ current += ch;
95
+ }
96
+ flush();
97
+ return optional;
98
+ }
99
+
100
+ /** Split a documented `(type)` group into single tokens. Both `|` and `,` are
101
+ * union separators — a README is as likely to write `a|b|nil` as `a, b, nil` —
102
+ * but only at depth 0, so a comma inside a token's own group (`table[k, v]`,
103
+ * `function(self, dt`) stays part of that token. `<`/`>` are deliberately not
104
+ * tracked: no group in the corpus uses them, and they need the `=>`
105
+ * disambiguation the comparator-side splitter carries. */
106
+ function splitTypes(group: string): string[] {
107
+ const tokens: string[] = [];
108
+ let depth = 0;
109
+ let current = "";
110
+ for (const ch of group) {
111
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
112
+ else if ((ch === ")" || ch === "]" || ch === "}") && depth > 0) depth--;
113
+ if ((ch === "|" || ch === ",") && depth === 0) {
114
+ tokens.push(current);
115
+ current = "";
116
+ continue;
117
+ }
118
+ current += ch;
119
+ }
120
+ tokens.push(current);
121
+ return tokens.map((token) => token.trim()).filter((token) => token.length > 0);
122
+ }
123
+
124
+ /** Parse one `* `name` (type) doc` bullet into a slot, splitting a `a|b|nil` or
125
+ * `a, b, nil` union into single tokens. Throws naming `fnName` when the bullet
126
+ * names a parameter but carries no `(type)`. */
127
+ function parseSlot(
128
+ label: string,
129
+ fnName: string,
130
+ line: string,
131
+ optionalNames: Set<string>,
132
+ ): MarkdownParam {
133
+ const typed = TYPED_BULLET.exec(line);
134
+ if (typed === null) {
135
+ const named = NAMED_BULLET.exec(line);
136
+ const name = named?.[1] ?? line.trim();
137
+ throw new Error(
138
+ `parse-markdown-api: ${label}: ${fnName} row for \`${name}\` has no (type) — cannot resolve to a typed param (row: ${line.trim()})`,
139
+ );
140
+ }
141
+ const name = typed[1] as string;
142
+ const types = splitTypes(typed[2] as string);
143
+ const slot: MarkdownParam = { name, doc: (typed[3] as string).trim(), types };
144
+ if (optionalNames.has(name)) slot.is_optional = "True";
145
+ return slot;
146
+ }
147
+
148
+ /** Split the README into `##`/`###` `<receiver>.<fn>(...)` sections, ignoring
149
+ * headers that are not dotted API signatures. */
150
+ function sections(lines: string[]): { header: RegExpExecArray; body: string[] }[] {
151
+ const starts: number[] = [];
152
+ lines.forEach((line, index) => {
153
+ if (HEADER.test(line)) starts.push(index);
154
+ });
155
+ return starts.map((start, i) => {
156
+ const end = i + 1 < starts.length ? (starts[i + 1] as number) : lines.length;
157
+ return {
158
+ header: HEADER.exec(lines[start] as string) as RegExpExecArray,
159
+ body: lines.slice(start + 1, end),
160
+ };
161
+ });
162
+ }
163
+
164
+ function parseSection(
165
+ label: string,
166
+ fnName: string,
167
+ body: string[],
168
+ optionalNames: Set<string>,
169
+ ): {
170
+ description: string;
171
+ parameters: MarkdownParam[];
172
+ returnvalues: MarkdownParam[];
173
+ } {
174
+ const descriptionLines: string[] = [];
175
+ const parameters: MarkdownParam[] = [];
176
+ const returnvalues: MarkdownParam[] = [];
177
+ let mode: "none" | "params" | "returns" = "none";
178
+ let sawMarker = false;
179
+
180
+ for (const line of body) {
181
+ if (PARAM_MARKER.test(line)) {
182
+ mode = "params";
183
+ sawMarker = true;
184
+ continue;
185
+ }
186
+ if (RETURN_MARKER.test(line)) {
187
+ mode = "returns";
188
+ sawMarker = true;
189
+ continue;
190
+ }
191
+ const isBullet = line.trimStart().startsWith("* ");
192
+ if (isBullet && mode === "params") {
193
+ parameters.push(parseSlot(label, fnName, line, optionalNames));
194
+ continue;
195
+ }
196
+ if (isBullet && mode === "returns") {
197
+ returnvalues.push(parseSlot(label, fnName, line, optionalNames));
198
+ continue;
199
+ }
200
+ // Any non-bullet line closes an open list, so a blank line before an
201
+ // option-table's `Acceptable values:` bullets stops them being captured.
202
+ mode = "none";
203
+ if (!sawMarker && line.trim().length > 0 && !line.startsWith("---")) {
204
+ descriptionLines.push(line.trim());
205
+ }
206
+ }
207
+
208
+ return { description: descriptionLines.join(" "), parameters, returnvalues };
209
+ }
210
+
211
+ /**
212
+ * `label` names the offending module in every loud-fail message. A library whose
213
+ * `.md` is usage/tutorial prose (defold-input ships six such modules) yields no
214
+ * signature section at all; emitting that as an empty namespace would silently
215
+ * publish a module with no members, so it throws instead.
216
+ */
217
+ export function parseMarkdownApi(text: string, label = "markdown document"): MarkdownDoc {
218
+ const elements: MarkdownElement[] = [];
219
+ const prefixes = new Set<string>();
220
+
221
+ for (const { header, body } of sections(text.split("\n"))) {
222
+ const prefix = header[1] as string;
223
+ const fn = header[2] as string;
224
+ const name = `${prefix}.${fn}`;
225
+ prefixes.add(prefix);
226
+ const { description, parameters, returnvalues } = parseSection(
227
+ label,
228
+ name,
229
+ body,
230
+ bracketedArgs(header[3] as string),
231
+ );
232
+ elements.push({ type: "FUNCTION", name, description, parameters, returnvalues });
233
+ }
234
+
235
+ if (prefixes.size > 1) {
236
+ throw new Error(
237
+ `parse-markdown-api: non-uniform module prefix across headers: ${[...prefixes].sort().join(", ")}`,
238
+ );
239
+ }
240
+ if (elements.length === 0) {
241
+ throw new Error(
242
+ `parse-markdown-api: ${label} has no \`##\`/\`###\` \`<receiver>.<fn>(...)\` API signature section — refusing to emit an empty namespace`,
243
+ );
244
+ }
245
+ const namespace = [...prefixes][0] ?? "";
246
+ return { info: { namespace, brief: "", description: "" }, elements };
247
+ }