@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 +21 -0
- package/README.md +48 -0
- package/package.json +40 -0
- package/src/index.d.ts +39 -0
- package/src/index.js +504 -0
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
|
+
};
|