@pieceful/ravel-noweb 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # `@pieceful/ravel-noweb`
2
+
3
+ Lossless, portable noweb input adapter for [Ravel](https://github.com/jostylr/ravel). It recognizes ordinary
4
+ `<<name>>=` definitions, repeated fragments, `<<name>>` references, `@`
5
+ terminators, preceding documentation, configured or filename-inferred
6
+ languages, and classic-compatible Ravel pragmas. Parsing never runs code or
7
+ writes tangled output.
8
+
9
+ ```js
10
+ import { nowebToMap } from "@pieceful/ravel-noweb";
11
+
12
+ const { map, diagnostics, surface } = nowebToMap(source, {
13
+ uri: "program.nw",
14
+ document: "program",
15
+ language: "javascript",
16
+ });
17
+ ```
18
+
19
+ The default `noweb` dialect treats pipes as part of classic chunk names.
20
+ `dialect: "noweb-plus"` enables definition and use-site pipelines:
21
+
22
+ ```noweb
23
+ <<main | trim()>>=
24
+ console.log(<<message | indent(2)>>);
25
+ @
26
+ ```
27
+
28
+ This extended spelling produces a portability warning. For a source that
29
+ classic noweb should continue to consume, declare the definition pipeline in
30
+ documentation and use underscore-quote references for piped uses:
31
+
32
+ ```noweb
33
+ @ %ravel pipeline main | trim()
34
+ <<main>>=
35
+ _"message | indent(2)"
36
+ @
37
+ ```
38
+
39
+ Other supported documentation pragmas are:
40
+
41
+ ```noweb
42
+ @ %ravel language main | javascript
43
+ @ %ravel output main | dist/main.js
44
+ @ %ravel run main | provider=quickjs-wasm-worker
45
+ ```
46
+
47
+ Live metadata is only planned. A host still owns provider selection and
48
+ execution.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@pieceful/ravel-noweb",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Lossless noweb and noweb-plus source adapter for Ravel.",
6
+ "license": "MIT",
7
+ "author": "James Taylor",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jostylr/ravel.git",
11
+ "directory": "packages/noweb"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/jostylr/ravel/issues"
15
+ },
16
+ "homepage": "https://ravel.jostylr.com/",
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "keywords": [
21
+ "ravel",
22
+ "noweb",
23
+ "literate-programming"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "exports": {
29
+ ".": "./src/index.js"
30
+ },
31
+ "types": "./src/index.d.ts",
32
+ "files": [
33
+ "src",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "dependencies": {
38
+ "@pieceful/ravel-core": "0.2.0"
39
+ }
40
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface SourcePosition { line: number; column: number; offset: number; }
2
+ export interface SourceRange { start: SourcePosition; end: SourcePosition; }
3
+ export interface SourceLocation { uri: string; range: SourceRange; }
4
+ export interface Diagnostic {
5
+ code: string;
6
+ severity: "error" | "warning" | "info";
7
+ message: string;
8
+ source: SourceLocation;
9
+ }
10
+ export interface NowebRunOptions {
11
+ run?: boolean;
12
+ provider?: string;
13
+ }
14
+ export interface NowebOptions {
15
+ uri?: string;
16
+ document?: string;
17
+ dialect?: "noweb" | "noweb-plus";
18
+ references?: "noweb" | "underscore-quote" | "both";
19
+ language?: string;
20
+ languages?: Record<string, string>;
21
+ run?: boolean | string[] | Record<string, boolean | NowebRunOptions>;
22
+ provider?: string;
23
+ }
24
+ export interface NowebAdapterResult {
25
+ map: {
26
+ version: 1;
27
+ document: { id: string; uri: string; format: string };
28
+ chunks: Array<Record<string, unknown>>;
29
+ directives: Array<Record<string, unknown>>;
30
+ metadata: Record<string, unknown>;
31
+ };
32
+ diagnostics: Diagnostic[];
33
+ surface: {
34
+ definitions: Array<Record<string, unknown>>;
35
+ references: Array<Record<string, unknown>>;
36
+ directives: Array<Record<string, unknown>>;
37
+ };
38
+ }
39
+ export function nowebToMap(text: string, options?: NowebOptions): NowebAdapterResult;
package/src/index.js ADDED
@@ -0,0 +1,504 @@
1
+ import { parseDefinitionPipeline } from "@pieceful/ravel-core";
2
+
3
+ const componentPattern = /^[a-z][a-z0-9-]*$/;
4
+ const dialects = new Set(["noweb", "noweb-plus"]);
5
+ const referencePolicies = new Set(["noweb", "underscore-quote", "both"]);
6
+
7
+ const diagnostic = (code, message, source, severity = "error") => ({
8
+ code, severity, message, source
9
+ });
10
+
11
+ const lineStarts = (text) => {
12
+ const starts = [0];
13
+ for (let index = 0; index < text.length; index += 1) {
14
+ if (text[index] === "\n") starts.push(index + 1);
15
+ }
16
+ return starts;
17
+ };
18
+
19
+ const positionAt = (starts, offset) => {
20
+ let low = 0;
21
+ let high = starts.length;
22
+ while (low + 1 < high) {
23
+ const middle = Math.floor((low + high) / 2);
24
+ if (starts[middle] <= offset) low = middle;
25
+ else high = middle;
26
+ }
27
+ return { line: low, column: offset - starts[low], offset };
28
+ };
29
+
30
+ const rangeAt = (uri, starts, start, end) => ({
31
+ uri,
32
+ range: { start: positionAt(starts, start), end: positionAt(starts, end) }
33
+ });
34
+
35
+ const advanceRange = (source, text, start, end) => {
36
+ const starts = lineStarts(text);
37
+ const base = source.range.start;
38
+ const position = (offset) => {
39
+ const local = positionAt(starts, offset);
40
+ return {
41
+ line: base.line + local.line,
42
+ column: local.line === 0 ? base.column + local.column : local.column,
43
+ offset: base.offset + offset
44
+ };
45
+ };
46
+ return { uri: source.uri, range: { start: position(start), end: position(end) } };
47
+ };
48
+
49
+ const linesOf = (text) => {
50
+ const lines = [];
51
+ let start = 0;
52
+ while (start < text.length) {
53
+ let end = text.indexOf("\n", start);
54
+ if (end === -1) end = text.length;
55
+ else end += 1;
56
+ const contentEnd = end > start && text[end - 1] === "\n"
57
+ ? end - (end > start + 1 && text[end - 2] === "\r" ? 2 : 1)
58
+ : end;
59
+ lines.push({ start, end, contentEnd, value: text.slice(start, contentEnd) });
60
+ start = end;
61
+ }
62
+ return lines;
63
+ };
64
+
65
+ const defaultDocumentId = (uri) => {
66
+ const base = uri.split(/[\\/]/).at(-1)?.replace(/\.[^.]+$/, "") ?? "";
67
+ const id = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
68
+ return componentPattern.test(id) ? id : null;
69
+ };
70
+
71
+ const semanticComponent = (value) => {
72
+ const normalized = value.trim().toLowerCase()
73
+ .normalize("NFKD")
74
+ .replace(/[\u0300-\u036f]/g, "")
75
+ .replace(/[^a-z0-9]+/g, "-")
76
+ .replace(/^-|-$/g, "");
77
+ return componentPattern.test(normalized) ? normalized : null;
78
+ };
79
+
80
+ const firstUnescapedPipe = (value) => {
81
+ let escaped = false;
82
+ for (let index = 0; index < value.length; index += 1) {
83
+ if (escaped) escaped = false;
84
+ else if (value[index] === "\\") escaped = true;
85
+ else if (value[index] === "|") return index;
86
+ }
87
+ return -1;
88
+ };
89
+
90
+ const splitNamePipeline = (value, extended) => {
91
+ const pipeIndex = extended ? firstUnescapedPipe(value) : -1;
92
+ return {
93
+ name: (pipeIndex === -1 ? value : value.slice(0, pipeIndex)).trim().replace(/\\\|/g, "|"),
94
+ pipe: pipeIndex === -1 ? null : value.slice(pipeIndex + 1).trim(),
95
+ pipeIndex
96
+ };
97
+ };
98
+
99
+ const pipelineKey = (pipeline) =>
100
+ JSON.stringify(pipeline.map(({ name, arguments: argumentsValue }) => [name, argumentsValue ?? []]));
101
+
102
+ const inferredLanguage = (name) => {
103
+ const match = /\.([A-Za-z][A-Za-z0-9+-]*)$/.exec(name.trim());
104
+ if (!match) return null;
105
+ const extension = match[1].toLowerCase();
106
+ return ({
107
+ c: "c", cc: "cpp", cpp: "cpp", cxx: "cpp", css: "css", go: "go",
108
+ html: "html", java: "java", js: "javascript", jsx: "javascript",
109
+ json: "json", lua: "lua", py: "python", rb: "ruby", rs: "rust",
110
+ sh: "shell", ts: "typescript", tsx: "typescript", xml: "xml"
111
+ })[extension] ?? extension;
112
+ };
113
+
114
+ const scanDefinitions = (text, uri, starts) => {
115
+ const lines = linesOf(text);
116
+ const definitions = [];
117
+ let documentationStart = 0;
118
+ for (let index = 0; index < lines.length; index += 1) {
119
+ const line = lines[index];
120
+ const opener = /^([ \t]*)<<(.+)>>=[ \t]*$/.exec(line.value);
121
+ if (!opener) continue;
122
+ let terminator = null;
123
+ let terminatorIndex = index + 1;
124
+ for (; terminatorIndex < lines.length; terminatorIndex += 1) {
125
+ if (/^@[ \t]*$/.test(lines[terminatorIndex].value)) {
126
+ terminator = lines[terminatorIndex];
127
+ break;
128
+ }
129
+ }
130
+ const bodyEnd = terminator?.start ?? text.length;
131
+ const nameStart = line.start + opener[1].length + 2;
132
+ definitions.push({
133
+ authoredHeader: opener[2],
134
+ nameStart,
135
+ declaration: rangeAt(uri, starts, line.start, line.end),
136
+ documentation: text.slice(documentationStart, line.start),
137
+ documentationSource: rangeAt(uri, starts, documentationStart, line.start),
138
+ body: text.slice(line.end, bodyEnd),
139
+ bodySource: rangeAt(uri, starts, line.end, bodyEnd),
140
+ terminator: terminator ? rangeAt(uri, starts, terminator.start, terminator.end) : null
141
+ });
142
+ if (!terminator) break;
143
+ documentationStart = terminator.end;
144
+ index = terminatorIndex;
145
+ }
146
+ return definitions;
147
+ };
148
+
149
+ const pragmasFrom = (text, source) => {
150
+ const pragmas = [];
151
+ const pattern = /^[ \t]*@[ \t]+%ravel[ \t]+(pipeline|language|output|run)[ \t]+(.+?)[ \t]*$/gm;
152
+ for (const match of text.matchAll(pattern)) {
153
+ const pipe = firstUnescapedPipe(match[2]);
154
+ const name = (pipe === -1 ? match[2] : match[2].slice(0, pipe)).trim().replace(/\\\|/g, "|");
155
+ pragmas.push({
156
+ kind: match[1],
157
+ name,
158
+ value: pipe === -1 ? null : match[2].slice(pipe + 1).trim(),
159
+ source: advanceRange(source, text, match.index, match.index + match[0].length)
160
+ });
161
+ }
162
+ return pragmas;
163
+ };
164
+
165
+ const referencesFrom = (body, source, extended) => {
166
+ const references = [];
167
+ let index = 0;
168
+ while (index < body.length) {
169
+ const start = body.indexOf("<<", index);
170
+ if (start === -1) break;
171
+ const end = body.indexOf(">>", start + 2);
172
+ if (end === -1) break;
173
+ const authored = body.slice(start + 2, end);
174
+ const split = splitNamePipeline(authored, extended);
175
+ references.push({
176
+ authored,
177
+ name: split.name,
178
+ pipe: split.pipe,
179
+ pipeIndex: split.pipeIndex,
180
+ source: advanceRange(source, body, start, end + 2)
181
+ });
182
+ index = end + 2;
183
+ }
184
+ return references;
185
+ };
186
+
187
+ const underscoreReferencesFrom = (body, source) => {
188
+ const references = [];
189
+ const pattern = /(?:\\[1-9][0-9]*)?_(["'`])([\s\S]*?)\1/g;
190
+ for (const match of body.matchAll(pattern)) {
191
+ const prefix = match[0].indexOf("_");
192
+ references.push({
193
+ targetText: match[2],
194
+ source: advanceRange(source, body, match.index + prefix, match.index + match[0].length)
195
+ });
196
+ }
197
+ return references;
198
+ };
199
+
200
+ const runConfiguration = (options, name, canonical) => {
201
+ if (options.run === true) return { run: true, provider: options.provider ?? null };
202
+ if (Array.isArray(options.run)) {
203
+ return { run: options.run.includes(name) || options.run.includes(canonical), provider: options.provider ?? null };
204
+ }
205
+ const entry = options.run && typeof options.run === "object"
206
+ ? options.run[name] ?? options.run[canonical]
207
+ : null;
208
+ if (entry === true) return { run: true, provider: options.provider ?? null };
209
+ if (entry && typeof entry === "object") {
210
+ return { run: entry.run !== false, provider: entry.provider ?? options.provider ?? null };
211
+ }
212
+ return { run: false, provider: null };
213
+ };
214
+
215
+ export const nowebToMap = (text, options = {}) => {
216
+ const uri = options.uri ?? "document.nw";
217
+ const starts = lineStarts(text);
218
+ const dialect = options.dialect ?? "noweb";
219
+ const references = options.references ?? (dialect === "noweb-plus" ? "both" : "noweb");
220
+ if (!dialects.has(dialect)) throw new Error("Unknown noweb dialect: " + dialect);
221
+ if (!referencePolicies.has(references)) throw new Error("Unknown noweb reference policy: " + references);
222
+ const documentId = options.document ?? defaultDocumentId(uri);
223
+ if (!componentPattern.test(documentId ?? "")) {
224
+ throw new Error("noweb document identity must be a lowercase identifier: " + String(documentId));
225
+ }
226
+
227
+ const diagnostics = [];
228
+ const scanned = scanDefinitions(text, uri, starts);
229
+ const extended = dialect === "noweb-plus";
230
+ const canonicalByName = new Map();
231
+ const ownerByCanonical = new Map();
232
+ const aliases = {};
233
+
234
+ const canonicalFor = (name, source) => {
235
+ if (canonicalByName.has(name)) return canonicalByName.get(name);
236
+ const base = semanticComponent(name);
237
+ if (!base) {
238
+ diagnostics.push(diagnostic("LPA101", "noweb chunk name does not produce a usable Ravel ID: " + name, source));
239
+ return null;
240
+ }
241
+ let canonical = base;
242
+ let suffix = 2;
243
+ while (ownerByCanonical.has(canonical) && ownerByCanonical.get(canonical) !== name) {
244
+ canonical = base + "-" + suffix;
245
+ suffix += 1;
246
+ }
247
+ if (canonical !== base) {
248
+ diagnostics.push(diagnostic(
249
+ "LPA102",
250
+ "noweb chunk names normalize to the same Ravel ID; " + name + " was assigned " + canonical + ".",
251
+ source
252
+ ));
253
+ }
254
+ canonicalByName.set(name, canonical);
255
+ ownerByCanonical.set(canonical, name);
256
+ aliases[name] = canonical;
257
+ return canonical;
258
+ };
259
+
260
+ for (const definition of scanned) {
261
+ definition.split = splitNamePipeline(definition.authoredHeader, extended);
262
+ definition.canonical = canonicalFor(definition.split.name, definition.declaration);
263
+ definition.references = referencesFrom(definition.body, definition.bodySource, extended);
264
+ if (!definition.terminator) {
265
+ diagnostics.push(diagnostic(
266
+ "LPA111",
267
+ "noweb definition is missing its `@` terminator.",
268
+ definition.declaration
269
+ ));
270
+ }
271
+ if (extended && definition.split.pipeIndex !== -1) {
272
+ diagnostics.push(diagnostic(
273
+ "LPA114",
274
+ "noweb-plus definition pipes are not interpreted as pipelines by classic noweb.",
275
+ definition.declaration,
276
+ "warning"
277
+ ));
278
+ }
279
+ }
280
+ for (const definition of scanned) {
281
+ for (const reference of definition.references) {
282
+ reference.canonical = canonicalFor(reference.name, reference.source);
283
+ if (extended && reference.pipeIndex !== -1) {
284
+ diagnostics.push(diagnostic(
285
+ "LPA114",
286
+ "noweb-plus reference pipes are not interpreted as pipelines by classic noweb.",
287
+ reference.source,
288
+ "warning"
289
+ ));
290
+ }
291
+ }
292
+ }
293
+
294
+ const chunks = [];
295
+ const chunksByCanonical = new Map();
296
+ const directives = [];
297
+ const surface = { definitions: [], references: [], directives: [] };
298
+ const pendingPragmas = [];
299
+
300
+ const parsePipeline = (value, source) => {
301
+ if (!value) return [];
302
+ const parsed = parseDefinitionPipeline(value, source);
303
+ diagnostics.push(...parsed.diagnostics);
304
+ return parsed.pipeline;
305
+ };
306
+
307
+ for (const definition of scanned) {
308
+ pendingPragmas.push(...pragmasFrom(definition.documentation, definition.documentationSource));
309
+ if (!definition.canonical) continue;
310
+ const matching = pendingPragmas.filter((pragma) => pragma.name === definition.split.name);
311
+ for (let index = pendingPragmas.length - 1; index >= 0; index -= 1) {
312
+ if (pendingPragmas[index].name === definition.split.name) pendingPragmas.splice(index, 1);
313
+ }
314
+ const pipelinePragmas = matching.filter((pragma) => pragma.kind === "pipeline");
315
+ const pragmaPipeline = pipelinePragmas.flatMap((pragma) => {
316
+ if (!pragma.value) {
317
+ diagnostics.push(diagnostic("LPA110", "Ravel pipeline pragmas require `name | pipeline`.", pragma.source));
318
+ return [];
319
+ }
320
+ return parsePipeline(pragma.value, pragma.source);
321
+ });
322
+ const inlinePipeline = parsePipeline(
323
+ definition.split.pipe,
324
+ rangeAt(
325
+ uri,
326
+ starts,
327
+ definition.nameStart + Math.max(0, definition.split.pipeIndex + 1),
328
+ definition.nameStart + definition.authoredHeader.length
329
+ )
330
+ );
331
+ if (inlinePipeline.length && pragmaPipeline.length &&
332
+ pipelineKey(inlinePipeline) !== pipelineKey(pragmaPipeline)) {
333
+ diagnostics.push(diagnostic("LPA113", "Inline and pragma definition pipelines conflict.", definition.declaration));
334
+ }
335
+ const declaredPipeline = inlinePipeline.length ? inlinePipeline : pragmaPipeline;
336
+ const id = documentId + "::" + definition.canonical;
337
+ let chunk = chunksByCanonical.get(definition.canonical);
338
+ if (!chunk) {
339
+ const configuredLanguage = options.languages?.[definition.split.name] ??
340
+ options.languages?.[definition.canonical] ?? options.language ?? null;
341
+ const languagePragma = matching.findLast((pragma) => pragma.kind === "language");
342
+ const language = languagePragma?.value ?? configuredLanguage ?? inferredLanguage(definition.split.name);
343
+ const languageSource = languagePragma
344
+ ? "pragma"
345
+ : configuredLanguage
346
+ ? "configuration"
347
+ : language
348
+ ? "chunk-name-extension"
349
+ : null;
350
+ const configuredRun = runConfiguration(options, definition.split.name, definition.canonical);
351
+ const runPragma = matching.findLast((pragma) => pragma.kind === "run");
352
+ const runValues = Object.fromEntries((runPragma?.value?.split("|") ?? [])
353
+ .map((value) => value.trim().split("=", 2))
354
+ .filter(([key, value]) => key && value));
355
+ chunk = {
356
+ id,
357
+ identity: { document: documentId, chunk: definition.canonical, minor: null, type: null },
358
+ name: definition.split.name,
359
+ body: "",
360
+ definitionPipeline: declaredPipeline,
361
+ metadata: {
362
+ ...(language ? { language } : {}),
363
+ tags: [],
364
+ data: {
365
+ ravel: {
366
+ adapter: "noweb",
367
+ dialect,
368
+ displayName: definition.split.name,
369
+ declarations: [],
370
+ documentation: [],
371
+ terminators: [],
372
+ referenceSyntax: {
373
+ noweb: references !== "underscore-quote",
374
+ underscore: references !== "noweb",
375
+ dialect,
376
+ aliases
377
+ },
378
+ ...(languageSource ? { languageSource } : {}),
379
+ ...((configuredRun.run || runPragma) ? { run: true } : {}),
380
+ ...((runValues.provider ?? configuredRun.provider)
381
+ ? { provider: runValues.provider ?? configuredRun.provider }
382
+ : {})
383
+ }
384
+ }
385
+ },
386
+ source: definition.bodySource,
387
+ fragments: [],
388
+ _pipelineKey: pipelineKey(declaredPipeline)
389
+ };
390
+ chunks.push(chunk);
391
+ chunksByCanonical.set(definition.canonical, chunk);
392
+ } else if (declaredPipeline.length) {
393
+ const key = pipelineKey(declaredPipeline);
394
+ if (chunk.definitionPipeline.length === 0) {
395
+ chunk.definitionPipeline = declaredPipeline;
396
+ chunk._pipelineKey = key;
397
+ } else if (chunk._pipelineKey !== key) {
398
+ diagnostics.push(diagnostic(
399
+ "LPA113",
400
+ "Repeated noweb definitions have conflicting pipelines for " + definition.split.name + ".",
401
+ definition.declaration
402
+ ));
403
+ }
404
+ }
405
+
406
+ chunk.body += definition.body;
407
+ chunk.fragments.push({ body: definition.body, source: definition.bodySource });
408
+ const ravel = chunk.metadata.data.ravel;
409
+ ravel.declarations.push(definition.declaration);
410
+ ravel.documentation.push({
411
+ text: definition.documentation,
412
+ source: definition.documentationSource
413
+ });
414
+ if (definition.terminator) ravel.terminators.push(definition.terminator);
415
+
416
+ const languagePragma = matching.findLast((pragma) => pragma.kind === "language");
417
+ const fragmentLanguage = languagePragma?.value ??
418
+ options.languages?.[definition.split.name] ??
419
+ options.languages?.[definition.canonical] ??
420
+ options.language ??
421
+ inferredLanguage(definition.split.name);
422
+ if (fragmentLanguage && chunk.metadata.language && fragmentLanguage !== chunk.metadata.language) {
423
+ diagnostics.push(diagnostic(
424
+ "LPA113",
425
+ "Repeated noweb definitions have conflicting languages for " + definition.split.name + ".",
426
+ definition.declaration
427
+ ));
428
+ } else if (fragmentLanguage && !chunk.metadata.language) {
429
+ chunk.metadata.language = fragmentLanguage;
430
+ }
431
+
432
+ surface.definitions.push({
433
+ pieceId: id,
434
+ declaration: definition.declaration,
435
+ fragments: [definition.bodySource],
436
+ displayName: definition.split.name,
437
+ documentation: definition.documentationSource
438
+ });
439
+ if (references !== "underscore-quote") {
440
+ for (const reference of definition.references) {
441
+ surface.references.push({
442
+ ownerPieceId: id,
443
+ targetText: reference.name,
444
+ source: reference.source
445
+ });
446
+ }
447
+ }
448
+ if (references !== "noweb") {
449
+ for (const reference of underscoreReferencesFrom(definition.body, definition.bodySource)) {
450
+ surface.references.push({
451
+ ownerPieceId: id,
452
+ targetText: reference.targetText,
453
+ source: reference.source
454
+ });
455
+ }
456
+ }
457
+
458
+ for (const pragma of matching.filter((entry) => entry.kind === "output")) {
459
+ if (!pragma.value) {
460
+ diagnostics.push(diagnostic("LPA110", "Ravel output pragmas require `name | path`.", pragma.source));
461
+ continue;
462
+ }
463
+ const directive = { kind: "out", name: pragma.value, from: id, source: pragma.source };
464
+ directives.push(directive);
465
+ surface.directives.push({ kind: "out", target: pragma.value, source: pragma.source });
466
+ }
467
+ }
468
+
469
+ if (scanned.length === 0) {
470
+ diagnostics.push(diagnostic(
471
+ "LPA100",
472
+ "No noweb chunk definitions were found.",
473
+ rangeAt(uri, starts, 0, text.length),
474
+ "warning"
475
+ ));
476
+ }
477
+ for (const pragma of pendingPragmas) {
478
+ diagnostics.push(diagnostic(
479
+ "LPA110",
480
+ "Ravel " + pragma.kind + " pragma did not match a following noweb definition: " + pragma.name,
481
+ pragma.source
482
+ ));
483
+ }
484
+
485
+ return {
486
+ map: {
487
+ version: 1,
488
+ document: { id: documentId, uri, format: dialect + "-v1" },
489
+ chunks: chunks.map(({ _pipelineKey, ...chunk }) => chunk),
490
+ directives,
491
+ metadata: {
492
+ adapter: "noweb",
493
+ dialect,
494
+ references,
495
+ documentation: scanned.map((definition) => ({
496
+ text: definition.documentation,
497
+ source: definition.documentationSource
498
+ }))
499
+ }
500
+ },
501
+ diagnostics,
502
+ surface
503
+ };
504
+ };