@weborigami/language 0.0.35

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 (69) hide show
  1. package/index.ts +30 -0
  2. package/main.js +21 -0
  3. package/package.json +24 -0
  4. package/src/compiler/code.d.ts +3 -0
  5. package/src/compiler/compile.js +55 -0
  6. package/src/compiler/origami.pegjs +277 -0
  7. package/src/compiler/parse.js +2292 -0
  8. package/src/compiler/parserHelpers.js +26 -0
  9. package/src/runtime/EventTargetMixin.d.ts +9 -0
  10. package/src/runtime/EventTargetMixin.js +117 -0
  11. package/src/runtime/ExpressionTree.js +20 -0
  12. package/src/runtime/FileLoadersTransform.d.ts +5 -0
  13. package/src/runtime/FileLoadersTransform.js +43 -0
  14. package/src/runtime/ImportModulesMixin.d.ts +5 -0
  15. package/src/runtime/ImportModulesMixin.js +48 -0
  16. package/src/runtime/InheritScopeMixin.js +34 -0
  17. package/src/runtime/InheritScopeMixin.ts +9 -0
  18. package/src/runtime/InvokeFunctionsTransform.d.ts +5 -0
  19. package/src/runtime/InvokeFunctionsTransform.js +27 -0
  20. package/src/runtime/OrigamiFiles.d.ts +11 -0
  21. package/src/runtime/OrigamiFiles.js +9 -0
  22. package/src/runtime/OrigamiTransform.d.ts +11 -0
  23. package/src/runtime/OrigamiTransform.js +11 -0
  24. package/src/runtime/OrigamiTree.js +4 -0
  25. package/src/runtime/ReadMe.md +1 -0
  26. package/src/runtime/Scope.js +89 -0
  27. package/src/runtime/TreeEvent.js +6 -0
  28. package/src/runtime/WatchFilesMixin.d.ts +5 -0
  29. package/src/runtime/WatchFilesMixin.js +58 -0
  30. package/src/runtime/concatTreeValues.js +46 -0
  31. package/src/runtime/evaluate.js +90 -0
  32. package/src/runtime/expressionFunction.js +33 -0
  33. package/src/runtime/extname.js +20 -0
  34. package/src/runtime/format.js +126 -0
  35. package/src/runtime/functionResultsMap.js +28 -0
  36. package/src/runtime/internal.js +20 -0
  37. package/src/runtime/ops.js +222 -0
  38. package/test/compiler/compile.test.js +64 -0
  39. package/test/compiler/parse.test.js +389 -0
  40. package/test/runtime/EventTargetMixin.test.js +68 -0
  41. package/test/runtime/ExpressionTree.test.js +27 -0
  42. package/test/runtime/FileLoadersTransform.test.js +41 -0
  43. package/test/runtime/InheritScopeMixin.test.js +29 -0
  44. package/test/runtime/OrigamiFiles.test.js +37 -0
  45. package/test/runtime/Scope.test.js +37 -0
  46. package/test/runtime/concatTreeValues.js +20 -0
  47. package/test/runtime/evaluate.test.js +55 -0
  48. package/test/runtime/fixtures/foo.js +1 -0
  49. package/test/runtime/fixtures/makeTest/a +1 -0
  50. package/test/runtime/fixtures/makeTest/b = a +0 -0
  51. package/test/runtime/fixtures/metagraphs/foo.txt +1 -0
  52. package/test/runtime/fixtures/metagraphs/greeting = this('world').js +3 -0
  53. package/test/runtime/fixtures/metagraphs/obj = this.json +5 -0
  54. package/test/runtime/fixtures/metagraphs/sample.txt = this().js +3 -0
  55. package/test/runtime/fixtures/metagraphs/string = this.json +1 -0
  56. package/test/runtime/fixtures/metagraphs/value = fn() +0 -0
  57. package/test/runtime/fixtures/programs/context.yaml +4 -0
  58. package/test/runtime/fixtures/programs/files.yaml +2 -0
  59. package/test/runtime/fixtures/programs/obj.yaml +3 -0
  60. package/test/runtime/fixtures/programs/simple.yaml +2 -0
  61. package/test/runtime/fixtures/subgraph = this.js +5 -0
  62. package/test/runtime/fixtures/templates/greet.orit +4 -0
  63. package/test/runtime/fixtures/templates/index.orit +15 -0
  64. package/test/runtime/fixtures/templates/names.yaml +3 -0
  65. package/test/runtime/fixtures/templates/plain.txt +1 -0
  66. package/test/runtime/fixtures/virtualKeys/.keys.json +1 -0
  67. package/test/runtime/format.test.js +66 -0
  68. package/test/runtime/functionResultsMap.test.js +27 -0
  69. package/test/runtime/ops.test.js +111 -0
package/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { AsyncTree } from "@weborigami/types";
2
+ import { StringLike } from "../async-tree/index.js";
3
+
4
+ export * from "./main.js";
5
+
6
+ /**
7
+ * A class constructor is an object with a `new` method that returns an
8
+ * instance of the indicated type.
9
+ */
10
+ export type Constructor<T> = new (...args: any[]) => T;
11
+
12
+ /**
13
+ * A function that can convert a string-like input value into some live object.
14
+ */
15
+ export type FileUnpackFunction = (
16
+ input: StringLike,
17
+ options?: { key?: any, parent?: AsyncTree | null }
18
+ ) => any;
19
+
20
+ /**
21
+ * A mixin is a function that takes an existing class and returns a new class.
22
+ *
23
+ * The use of a generic type `T` here is a way of indicating that the members of
24
+ * the supplied base class automatically pass through to the result. That
25
+ * ensures the use of the mixin doesn't accidentally hide members of the class
26
+ * passed to the mixin.
27
+ */
28
+ export type Mixin<MixinMembers> = <T>(
29
+ Base: Constructor<T>
30
+ ) => Constructor<T & MixinMembers>;
package/main.js ADDED
@@ -0,0 +1,21 @@
1
+ export * from "./src/runtime/internal.js";
2
+
3
+ export * as compile from "./src/compiler/compile.js";
4
+ export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
5
+ export { default as ExpressionTree } from "./src/runtime/ExpressionTree.js";
6
+ export { default as FileLoadersTransform } from "./src/runtime/FileLoadersTransform.js";
7
+ export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
8
+ export { default as InheritScopeMixin } from "./src/runtime/InheritScopeMixin.js";
9
+ export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
10
+ export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
11
+ export { default as OrigamiTransform } from "./src/runtime/OrigamiTransform.js";
12
+ export { default as OrigamiTree } from "./src/runtime/OrigamiTree.js";
13
+ export { default as Scope } from "./src/runtime/Scope.js";
14
+ export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
15
+ export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
16
+ export { default as concatTreeValues } from "./src/runtime/concatTreeValues.js";
17
+ export { default as evaluate } from "./src/runtime/evaluate.js";
18
+ export * as expressionFunction from "./src/runtime/expressionFunction.js";
19
+ export { default as extname } from "./src/runtime/extname.js";
20
+ export { default as format } from "./src/runtime/format.js";
21
+ export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@weborigami/language",
3
+ "version": "0.0.35",
4
+ "description": "Graph Origami expression language compiler and runtime",
5
+ "type": "module",
6
+ "main": "./main.js",
7
+ "types": "./index.ts",
8
+ "devDependencies": {
9
+ "@types/node": "20.8.10",
10
+ "typescript": "5.2.2"
11
+ },
12
+ "dependencies": {
13
+ "@weborigami/types": "0.0.35",
14
+ "@weborigami/async-tree": "https://gitpkg.now.sh/GraphOrigami/origami/async-tree?main",
15
+ "peggy": "3.0.2",
16
+ "watcher": "2.3.0"
17
+ },
18
+ "scripts": {
19
+ "build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "node --test",
22
+ "typecheck": "tsc"
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { Treelike } from "@weborigami/async-tree";
2
+
3
+ type Code = [Treelike, ...any[]] | any;
@@ -0,0 +1,55 @@
1
+ import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
+ import { parse } from "./parse.js";
3
+
4
+ function compile(text, startRule) {
5
+ // Trim whitespace from template blocks before we begin lexing, as our
6
+ // heuristics are non-local and hard to implement in our parser.
7
+ const preprocessed = trimTemplateWhitespace(text);
8
+ const code = parse(preprocessed, { startRule });
9
+ const fn = createExpressionFunction(code);
10
+ return fn;
11
+ }
12
+
13
+ export function expression(text) {
14
+ return compile(text, "expression");
15
+ }
16
+
17
+ export function templateDocument(text) {
18
+ return compile(text, "templateDocument");
19
+ }
20
+
21
+ // Trim the whitespace around and in substitution blocks in a template. There's
22
+ // no explicit syntax for blocks, but we infer them as any place where a
23
+ // substitution itself contains a multi-line template literal.
24
+ //
25
+ // Example:
26
+ //
27
+ // {{ if `
28
+ // true text
29
+ // `, `
30
+ // false text
31
+ // ` }}
32
+ //
33
+ // Case 1: a substitution that starts the text or starts a line (there's only
34
+ // whitespace before the `{{`), and has the line end with the start of a
35
+ // template literal (there's only whitespace after the backtick) marks the start
36
+ // of a block.
37
+ //
38
+ // Case 2: a line in the middle that ends one template literal and starts
39
+ // another is an internal break in the block. Edge case: three backticks in a
40
+ // row, like ```, are common in markdown and are not treated as a break.
41
+ //
42
+ // Case 3: a line that ends a template literal and ends with `}}` or ends the
43
+ // text marks the end of the block.
44
+ //
45
+ // In all three cases, we trim spaces and tabs from the start and end of the
46
+ // line. In case 1, we also remove the preceding newline.
47
+ function trimTemplateWhitespace(text) {
48
+ const regex1 = /(^|\n)[ \t]*({{.*?`)[ \t]*\n/g;
49
+ const regex2 = /\n[ \t]*(`(?!`).*?`)[ \t]*\n/g;
50
+ const regex3 = /\n[ \t]*(`(?!`).*?}})[ \t]*(?:\n|$)/g;
51
+ const trimBlockStarts = text.replace(regex1, "$1$2");
52
+ const trimBlockBreaks = trimBlockStarts.replace(regex2, "\n$1");
53
+ const trimBlockEnds = trimBlockBreaks.replace(regex3, "\n$1");
54
+ return trimBlockEnds;
55
+ }
@@ -0,0 +1,277 @@
1
+ {{
2
+ //
3
+ // Origami language parser
4
+ //
5
+ // Generate the parser via `npm build`.
6
+ // @ts-nocheck
7
+ //
8
+
9
+ import * as ops from "../runtime/ops.js";
10
+ import { makeFunctionCall, makeTemplate } from "./parserHelpers.js";
11
+ }}
12
+
13
+ // A block of optional whitespace
14
+ __
15
+ = (inlineSpace / newLine / comment)* { return ""; }
16
+
17
+ // A filesystem path that begins with a slash: `/foo/bar`
18
+ absoluteFilePath
19
+ = path:leadingSlashPath { return [[ops.filesRoot], ...path]; }
20
+
21
+ // A chain of arguments: `(arg1)(arg2)(arg3)`
22
+ argsChain
23
+ = parts:(parensArgs / leadingSlashPath)+ { return parts; }
24
+
25
+ // An assignment statement: `foo = 1`
26
+ assignment
27
+ = @identifier __ "=" __ @expr
28
+
29
+ assignmentOrShorthand
30
+ = assignment
31
+ / key:identifier { return [key, [ops.inherited, key]]; }
32
+
33
+ array
34
+ = "[" __ list:list? "]" { return [ops.array, ...(list ?? [])]; }
35
+
36
+ // Something that can be called. This is more restrictive than the `expr`
37
+ // parser; it doesn't accept regular function calls.
38
+ callTarget
39
+ = absoluteFilePath
40
+ / array
41
+ / object
42
+ / tree
43
+ / lambda
44
+ / protocolCall
45
+ / group
46
+ / scopeReference
47
+
48
+ // A single line comment
49
+ comment
50
+ = "#" [^\n\r]*
51
+
52
+ digits
53
+ = @[0-9]+
54
+
55
+ doubleQuoteString
56
+ = '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
57
+
58
+ doubleQuoteStringChar
59
+ = !('"' / newLine) @textChar
60
+
61
+ escapedChar
62
+ = "\\" @.
63
+
64
+ // An Origami expression, no leading/trailing whitespace
65
+ expr
66
+ // Try function calls first, as they can start with expression types that
67
+ // follow (array, object, etc.); we want to parse the largest thing first.
68
+ = implicitParensCall
69
+ / functionComposition
70
+ // Then try parsers that look for a distinctive token at the start: an opening
71
+ // slash, bracket, curly brace, etc.
72
+ / absoluteFilePath
73
+ / array
74
+ / object
75
+ / tree
76
+ / lambda
77
+ / templateLiteral
78
+ / group
79
+ / string
80
+ / number
81
+ // Protocol calls are distinguished by a colon, but it's not at the start.
82
+ / protocolCall
83
+ // Least distinctive option is a simple scope reference, so it comes last.
84
+ / scopeReference
85
+
86
+ // Top-level Origami expression, possible leading/trailing whitepsace.
87
+ expression "Origami expression"
88
+ = __ @expr __
89
+
90
+ float
91
+ = sign? digits? "." digits {
92
+ return parseFloat(text());
93
+ }
94
+
95
+ // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
96
+ // of function calls, like `fn(arg1)(arg2)(arg3)`.
97
+ functionComposition
98
+ = target:callTarget chain:argsChain { return makeFunctionCall(target, chain); }
99
+
100
+ // An expression in parentheses: `(foo)`
101
+ group
102
+ = "(" __ @expr __ ")"
103
+
104
+ identifier
105
+ = chars:identifierChar+ { return chars.join(""); }
106
+
107
+ identifierChar
108
+ = [^(){}\[\],/:=\`"'\\# \t\n\r] // No unescaped whitespace or special chars
109
+ / escapedChar
110
+
111
+ // A function call with implicit parentheses: `fn 1, 2, 3`
112
+ implicitParensCall
113
+ = target:(functionComposition / callTarget) inlineSpace+ args:list {
114
+ return [target, ...args];
115
+ }
116
+
117
+ // A host identifier that may include a colon and port number: `example.com:80`.
118
+ // This is used as a special case at the head of a path, where we want to
119
+ // interpret a colon as part of a text identifier.
120
+ host
121
+ = identifier (":" number)? { return text(); }
122
+
123
+ inlineSpace
124
+ = [ \t]
125
+
126
+ integer
127
+ = sign? digits {
128
+ return parseInt(text());
129
+ }
130
+
131
+ // A lambda expression: `=foo()`
132
+ lambda
133
+ = "=" __ expr:expr { return [ops.lambda, expr]; }
134
+
135
+ // A path that begins with a slash: `/foo/bar`
136
+ leadingSlashPath
137
+ = "/" @path
138
+ / "/" { return [""]; }
139
+
140
+ // A separated list of expressions
141
+ list = head:expr tail:(separator @expr)* separator? { return [head].concat(tail); }
142
+
143
+ newLine
144
+ = "\n"
145
+ / "\r\n"
146
+ / "\r"
147
+
148
+ // A number
149
+ number
150
+ = float
151
+ / integer
152
+
153
+ // An object literal: `{foo: 1, bar: 2}`
154
+ //
155
+ // TODO: Use Object.fromEntries with array of key/value pairs
156
+ //
157
+ object
158
+ = "{" __ properties:objectProperties? "}" { return [ops.object, ...(properties ?? [])]; }
159
+
160
+ // A separated list of object properties or shorthands
161
+ objectProperties
162
+ = head:objectPropertyOrShorthand tail:(separator @objectPropertyOrShorthand)* separator? __ {
163
+ return [head].concat(tail);
164
+ }
165
+
166
+ // A single object property with key and value: `x: 1`
167
+ objectProperty
168
+ = @identifier __ ":" __ @expr
169
+
170
+ objectPropertyOrShorthand
171
+ = objectProperty
172
+ / key:identifier { return [key, [ops.inherited, key]]; }
173
+
174
+ // Function arguments in parentheses
175
+ parensArgs
176
+ = "(" __ list:list? ")" { return list ?? [undefined]; }
177
+
178
+ separator
179
+ = __ "," __
180
+ / whitespaceWithNewLine
181
+
182
+ sign
183
+ = [+\-]
184
+
185
+ // A slash-separated path of keys
186
+ path
187
+ = head:pathKey "/" tail:path { return [head].concat(tail); }
188
+ / key:pathKey { return [key]; }
189
+
190
+ // A single key in a slash-separated path
191
+ pathKey
192
+ = key:identifierChar* { return key.join(""); }
193
+
194
+ // Parse a protocol call like `fn://foo/bar`.
195
+ // There can be zero, one, or two slashes after the colon.
196
+ protocolCall
197
+ = protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
198
+ return [protocol, host, ...(path ?? [])];
199
+ }
200
+
201
+ protocol
202
+ = reservedProtocol
203
+ / scopeReference
204
+
205
+ reservedProtocol
206
+ = "https" { return ops.https; }
207
+ / "http" { return ops.http; }
208
+ / "treehttps" { return ops.treeHttps; }
209
+ / "treehttp" { return ops.treeHttp; }
210
+ / "tree" { return ops.treeHttps; } // Shorthand alias
211
+
212
+ scopeReference
213
+ = key:identifier { return [ops.scope, key]; }
214
+
215
+ singleQuoteString
216
+ = "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
217
+
218
+ singleQuoteStringChar
219
+ = !("'" / newLine) @textChar
220
+
221
+ start
222
+ = number
223
+
224
+ string
225
+ = doubleQuoteString
226
+ / singleQuoteString
227
+
228
+ // A top-level document defining a template. This is the same as a template
229
+ // literal, but can contain backticks at the top level.
230
+ templateDocument "Origami template"
231
+ = contents:templateDocumentContents { return [ops.lambda, contents]; }
232
+
233
+ // Template documents can contain backticks at the top level.
234
+ templateDocumentChar
235
+ = !"{{" @textChar
236
+
237
+ // The contents of a template document containing plain text and substitutions
238
+ templateDocumentContents
239
+ = parts:(templateDocumentText / templateSubstitution)* { return makeTemplate(parts); }
240
+
241
+ templateDocumentText
242
+ = chars:templateDocumentChar+ { return chars.join(""); }
243
+
244
+ // A backtick-quoted template literal
245
+ templateLiteral
246
+ = "`" @templateLiteralContents "`"
247
+
248
+ templateLiteralChar
249
+ = !("`" / "{{") @textChar
250
+
251
+ // The contents of a template literal containing plain text and substitutions
252
+ templateLiteralContents
253
+ = parts:(templateLiteralText / templateSubstitution)* { return makeTemplate(parts); }
254
+
255
+ // Plain text in a template literal
256
+ templateLiteralText
257
+ = chars:templateLiteralChar+ { return chars.join(""); }
258
+
259
+ // A substitution in a template literal: `{{ fn() }}`
260
+ templateSubstitution
261
+ = "{{" @expression "}}"
262
+
263
+ textChar
264
+ = escapedChar / .
265
+
266
+ // A tree literal: `{ index.html = "Hello" }`
267
+ tree
268
+ = "{" __ assignments:treeAssignments? "}" { return [ops.tree, ...(assignments ?? [])]; }
269
+
270
+ // A separated list of assignments or shorthands
271
+ treeAssignments
272
+ = head:assignmentOrShorthand tail:(separator @assignmentOrShorthand)* separator? __ {
273
+ return [head].concat(tail);
274
+ }
275
+
276
+ whitespaceWithNewLine
277
+ = inlineSpace* comment? newLine __