@weborigami/origami 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/origami",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Web Origami language, CLI, framework, and server",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,22 +13,22 @@
13
13
  "main": "./main.js",
14
14
  "types": "./index.ts",
15
15
  "devDependencies": {
16
- "@types/node": "22.7.4",
17
- "typescript": "5.6.2"
16
+ "@types/node": "22.10.2",
17
+ "typescript": "5.7.2"
18
18
  },
19
19
  "dependencies": {
20
- "@weborigami/async-tree": "0.2.2",
21
- "@weborigami/language": "0.2.2",
22
- "@weborigami/types": "0.2.2",
20
+ "@weborigami/async-tree": "0.2.3",
21
+ "@weborigami/language": "0.2.3",
22
+ "@weborigami/types": "0.2.3",
23
23
  "exif-parser": "0.1.12",
24
24
  "graphviz-wasm": "3.0.2",
25
- "highlight.js": "11.10.0",
26
- "marked": "14.1.2",
27
- "marked-gfm-heading-id": "4.1.0",
28
- "marked-highlight": "2.1.4",
29
- "marked-smartypants": "1.1.8",
25
+ "highlight.js": "11.11.0",
26
+ "marked": "15.0.4",
27
+ "marked-gfm-heading-id": "4.1.1",
28
+ "marked-highlight": "2.2.1",
29
+ "marked-smartypants": "1.1.9",
30
30
  "sharp": "0.33.5",
31
- "yaml": "2.5.1"
31
+ "yaml": "2.6.1"
32
32
  },
33
33
  "scripts": {
34
34
  "test": "node --test --test-reporter=spec",
@@ -16,9 +16,8 @@ export default function processUnpackedContent(content, parent) {
16
16
  // Bind the function to the parent as the `this` context.
17
17
  const target = parent ?? builtinsTree;
18
18
  const result = content.bind(target);
19
- if (content.code) {
20
- result.code = content.code;
21
- }
19
+ // Copy over any properties that were attached to the function
20
+ Object.assign(result, content);
22
21
  return result;
23
22
  } else if (Tree.isAsyncTree(content) && !content.parent) {
24
23
  const result = Object.create(content);
@@ -2,6 +2,6 @@ import type { AsyncTree } from "@weborigami/types";
2
2
  import type { JsonValue } from "../../index.ts";
3
3
 
4
4
  export function evaluateYaml(text: string, parent?: AsyncTree|null): Promise<JsonValue>;
5
- export function parseYaml(text: string): JsonValue|AsyncTree;
5
+ export function parseYaml(text: string): JsonValue;
6
6
  export function toJson(obj: JsonValue | AsyncTree): Promise<string>;
7
7
  export function toYaml(obj: JsonValue | AsyncTree): Promise<string>;
@@ -30,7 +30,7 @@ export async function evaluateYaml(text, parent) {
30
30
 
31
31
  /**
32
32
  * @param {string} text
33
- * @returns {JsonValue|AsyncTree}
33
+ * @returns {JsonValue}
34
34
  */
35
35
  export function parseYaml(text) {
36
36
  return YAML.parse(text);
@@ -1,7 +1,14 @@
1
- import { symbols } from "@weborigami/async-tree";
1
+ import {
2
+ extension,
3
+ ObjectTree,
4
+ symbols,
5
+ trailingSlash,
6
+ } from "@weborigami/async-tree";
2
7
  import { compile } from "@weborigami/language";
3
- import * as utilities from "../common/utilities.js";
8
+ import { parseYaml } from "../common/serialize.js";
9
+ import { toString } from "../common/utilities.js";
4
10
  import { processUnpackedContent } from "../internal.js";
11
+ import parseFrontMatter from "./parseFrontMatter.js";
5
12
 
6
13
  /**
7
14
  * An Origami template document: a plain text file that contains Origami
@@ -15,29 +22,107 @@ export default {
15
22
  const parent =
16
23
  options.parent ??
17
24
  /** @type {any} */ (packed).parent ??
18
- /** @type {any} */ (packed)[symbols.parent];
25
+ /** @type {any} */ (packed)[symbols.parent] ??
26
+ null;
19
27
 
20
- // Construct an object to represent the source code.
21
- const sourceName = options.key;
28
+ // Unpack as a text document
29
+ const unpacked = toString(packed);
30
+
31
+ // See if we can construct a URL to use in error messages
32
+ const key = options.key;
22
33
  let url;
23
- if (sourceName && parent?.url) {
34
+ if (key && parent?.url) {
24
35
  let parentHref = parent.url.href;
25
36
  if (!parentHref.endsWith("/")) {
26
37
  parentHref += "/";
27
38
  }
28
- url = new URL(sourceName, parentHref);
39
+ url = new URL(key, parentHref);
40
+ }
41
+
42
+ // Determine the data (if present) and text content
43
+ let text;
44
+ let frontData = null;
45
+ let frontSource = null;
46
+ let extendedParent = parent;
47
+ const parsed = parseFrontMatter(unpacked);
48
+ if (!parsed) {
49
+ text = unpacked;
50
+ } else {
51
+ const { body, frontText, isOrigami } = parsed;
52
+ if (isOrigami) {
53
+ // Origami front matter
54
+ frontSource = { name: key, text: frontText, url };
55
+ } else {
56
+ // YAML front matter
57
+ frontData = parseYaml(frontText);
58
+ if (typeof frontData !== "object") {
59
+ throw new TypeError(`YAML or JSON front matter must be an object`);
60
+ }
61
+ extendedParent = new ObjectTree(frontData);
62
+ extendedParent.parent = parent;
63
+ }
64
+ text = body;
29
65
  }
30
66
 
31
- const source = {
32
- text: utilities.toString(packed),
33
- name: options.key,
34
- url,
35
- };
67
+ // Construct an object to represent the source code
68
+ const bodySource = { name: key, text, url };
36
69
 
37
- // Compile the text as an Origami template document.
38
- const templateDefineFn = compile.templateDocument(source);
39
- const templateFn = await templateDefineFn.call(parent);
70
+ // Compile the source as an Origami template document
71
+ const scopeCaching = frontSource ? false : true;
72
+ const defineTemplateFn = compile.templateDocument(bodySource, {
73
+ scopeCaching,
74
+ });
40
75
 
41
- return processUnpackedContent(templateFn, parent);
76
+ // Determine the result of the template
77
+ let result;
78
+ if (frontSource) {
79
+ // Result is the evaluated front source
80
+ const frontFn = compile.expression(frontSource, {
81
+ macros: {
82
+ "@template": defineTemplateFn.code,
83
+ },
84
+ });
85
+ result = await frontFn.call(parent);
86
+ } else {
87
+ const templateFn = await defineTemplateFn.call(extendedParent);
88
+ if (frontData) {
89
+ // Result is a function that adds the front data to the template result
90
+ result = async (input) => {
91
+ const text = await templateFn.call(extendedParent, input);
92
+ const object = {
93
+ ...frontData,
94
+ "@text": text,
95
+ };
96
+ object[symbols.parent] = extendedParent;
97
+ return object;
98
+ };
99
+ } else {
100
+ // Result is a function that calls the body template
101
+ result = templateFn;
102
+ }
103
+ }
104
+
105
+ const resultExtension = key ? extension.extname(key) : null;
106
+ if (resultExtension && Object.isExtensible(result)) {
107
+ // Add sidecar function so this template can be used in a map.
108
+ result.key = addExtension(resultExtension);
109
+ }
110
+
111
+ return processUnpackedContent(result, parent);
42
112
  },
43
113
  };
114
+
115
+ // Return a function that adds the given extension
116
+ function addExtension(resultExtension) {
117
+ return (sourceKey) => {
118
+ if (sourceKey === undefined) {
119
+ return undefined;
120
+ }
121
+ const normalizedKey = trailingSlash.remove(sourceKey);
122
+ const sourceExtension = extension.extname(normalizedKey);
123
+ const resultKey = sourceExtension
124
+ ? extension.replace(normalizedKey, sourceExtension, resultExtension)
125
+ : normalizedKey + resultExtension;
126
+ return resultKey;
127
+ };
128
+ }
@@ -0,0 +1,21 @@
1
+ export default function parseFrontMatter(text) {
2
+ const regex =
3
+ /^(---\r?\n(?<frontText>[\s\S]*?\r?\n?)---\r?\n)(?<body>[\s\S]*$)/;
4
+ const match = regex.exec(text);
5
+ if (!match?.groups) {
6
+ return null;
7
+ }
8
+ const isOrigami = detectOrigami(match.groups.frontText);
9
+ return {
10
+ body: match.groups.body,
11
+ frontText: match.groups.frontText,
12
+ isOrigami,
13
+ };
14
+ }
15
+
16
+ function detectOrigami(text) {
17
+ // Find first character that's not whitespace, alphanumeric, or underscore
18
+ const first = text.match(/[^A-Za-z0-9_ \t\n\r]/)?.[0];
19
+ const origamiMarkers = ["(", ".", "/", "{"];
20
+ return origamiMarkers.includes(first);
21
+ }
@@ -1,6 +1,8 @@
1
1
  import { isPacked, symbols } from "@weborigami/async-tree";
2
+ import { compile } from "@weborigami/language";
2
3
  import { parseYaml, toYaml } from "../common/serialize.js";
3
- import * as utilities from "../common/utilities.js";
4
+ import { toString } from "../common/utilities.js";
5
+ import parseFrontMatter from "./parseFrontMatter.js";
4
6
 
5
7
  /**
6
8
  * A text file with possible front matter
@@ -51,21 +53,25 @@ export default {
51
53
  },
52
54
 
53
55
  /** @type {import("@weborigami/language").UnpackFunction} */
54
- unpack(packed, options = {}) {
56
+ async unpack(packed, options = {}) {
55
57
  const parent = options.parent ?? null;
56
- const text = utilities.toString(packed);
58
+ const text = toString(packed);
57
59
  if (text === null) {
58
- throw new Error("Tried to treat something as text but it wasn't text.");
60
+ throw new Error("Tried to treat a file as text but it wasn't text.");
59
61
  }
60
62
 
61
- const regex =
62
- /^(---\r?\n(?<frontText>[\s\S]*?\r?\n?)---\r?\n)(?<body>[\s\S]*$)/;
63
- const match = regex.exec(text);
63
+ const parsed = parseFrontMatter(text);
64
64
  let unpacked;
65
- if (match) {
65
+ if (parsed) {
66
66
  // Document object with front matter
67
- const { body, frontText } = /** @type {any} */ (match.groups);
68
- const frontData = parseYaml(frontText);
67
+ const { body, frontText, isOrigami } = parsed;
68
+ let frontData;
69
+ if (isOrigami) {
70
+ const compiled = compile.expression(frontText.trim());
71
+ frontData = await compiled.call(parent);
72
+ } else {
73
+ frontData = parseYaml(frontText);
74
+ }
69
75
  unpacked = Object.assign({}, frontData, { "@text": body });
70
76
  } else {
71
77
  // Plain text
package/src/help/help.js CHANGED
@@ -50,7 +50,7 @@ async function commandDescription(commandHelp, namespace, command) {
50
50
  "",
51
51
  formatCommandDescription(commandHelp, namespace, command),
52
52
  "",
53
- `For more information: https://weborigami.org/builtins/${url}`,
53
+ `For more information: https://weborigami.org/builtins/${url}\n`,
54
54
  ];
55
55
  return text.join("\n");
56
56
  }
@@ -82,7 +82,7 @@ async function namespaceCommands(namespaceHelp, namespace) {
82
82
  }
83
83
 
84
84
  const url = namespaceHelp.collection ? `${namespace}.html` : `${namespace}/`;
85
- text.push(`For more information: https://weborigami.org/builtins/${url}`);
85
+ text.push(`For more information: https://weborigami.org/builtins/${url}\n`);
86
86
  return text.join("\n");
87
87
  }
88
88
 
@@ -97,7 +97,7 @@ async function namespaceDescriptions(helpData) {
97
97
  }
98
98
  }
99
99
  text.push(
100
- `\nType "ori help:<namespace>" for more or visit https://weborigami.org/builtins`
100
+ `\nType "ori help:<namespace>" for more or visit https://weborigami.org/builtins\n`
101
101
  );
102
102
  return text.join("\n");
103
103
  }
@@ -1,9 +1,12 @@
1
- import { isUnpackable, symbols } from "@weborigami/async-tree";
2
- import { compile } from "@weborigami/language";
1
+ import {
2
+ isUnpackable,
3
+ ObjectTree,
4
+ symbols,
5
+ toString,
6
+ } from "@weborigami/async-tree";
3
7
  import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
4
8
  import documentObject from "../common/documentObject.js";
5
- import { toString } from "../common/utilities.js";
6
- import { oriHandler } from "../internal.js";
9
+ import { oridocumentHandler } from "../internal.js";
7
10
 
8
11
  /**
9
12
  * Inline any Origami expressions found inside ${...} placeholders in the input
@@ -29,10 +32,19 @@ export default async function inline(input) {
29
32
  }
30
33
 
31
34
  const parent =
32
- /** @type {any} */ (input).parent ?? input[symbols.parent] ?? this;
33
- const templateFn = await oriHandler.unpack(origami, {
34
- compiler: compile.templateDocument,
35
- parent,
35
+ /** @type {any} */ (input).parent ??
36
+ /** @type {any} */ (input)[symbols.parent] ??
37
+ this;
38
+
39
+ let extendedParent = parent;
40
+ if (inputIsDocument) {
41
+ extendedParent = new ObjectTree(input);
42
+ extendedParent.parent = parent;
43
+ }
44
+
45
+ // @ts-ignore
46
+ const templateFn = await oridocumentHandler.unpack(input, {
47
+ parent: extendedParent,
36
48
  });
37
49
 
38
50
  const inputData = inputIsDocument ? input : null;
@@ -11,6 +11,7 @@ import origamiHighlightDefinition from "./origamiHighlightDefinition.js";
11
11
  highlight.registerLanguage("ori", origamiHighlightDefinition);
12
12
 
13
13
  marked.use(
14
+ // @ts-ignore
14
15
  markedGfmHeadingId(),
15
16
  markedHighlight({
16
17
  highlight(code, lang) {
package/src/text/text.js CHANGED
@@ -1,4 +1,4 @@
1
+ export { taggedTemplateIndent as indent } from "@weborigami/language";
1
2
  export { default as document } from "./document.js";
2
- export { default as indent } from "./indent.js";
3
3
  export { default as inline } from "./inline.js";
4
4
  export { default as mdHtml } from "./mdHtml.js";
@@ -1,4 +1,4 @@
1
- import { FunctionTree } from "@weborigami/async-tree";
1
+ import { FunctionTree, isUnpackable } from "@weborigami/async-tree";
2
2
  import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
3
3
  import { toFunction } from "../common/utilities.js";
4
4
 
@@ -20,6 +20,9 @@ export default async function fromFn(invocable, keys = []) {
20
20
  );
21
21
  }
22
22
  const fn = toFunction(invocable);
23
+ if (isUnpackable(keys)) {
24
+ keys = await keys.unpack();
25
+ }
23
26
  const tree = new FunctionTree(fn, keys);
24
27
  tree.parent = this;
25
28
  return tree;
package/src/tree/map.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TreeMapOptions as AsyncTreeMapOptions, Treelike, TreeTransform, ValueKeyFn } from "@weborigami/async-tree";
1
+ import { TreeMapOptions as AsyncTreeMapOptions, Treelike, ValueKeyFn } from "@weborigami/async-tree";
2
2
  import { AsyncTree } from "@weborigami/types";
3
3
 
4
4
  /* Add more properties to TreeMapOptions */
@@ -8,6 +8,4 @@ type TreeMapOptions = AsyncTreeMapOptions &{
8
8
  needsSourceValue?: boolean;
9
9
  };
10
10
 
11
- export default function treeMap(options: ValueKeyFn | TreeMapOptions): TreeTransform;
12
11
  export default function treeMap(treelike: Treelike, options: ValueKeyFn | TreeMapOptions): AsyncTree;
13
- export default function treeMap(param1: Treelike | ValueKeyFn | TreeMapOptions, param2?: ValueKeyFn | TreeMapOptions): AsyncTree | TreeTransform;
package/src/tree/map.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  cachedKeyFunctions,
3
3
  isPlainObject,
4
+ isUnpackable,
4
5
  keyFunctionsForExtensions,
5
6
  map as mapTransform,
6
7
  } from "@weborigami/async-tree";
@@ -25,7 +26,11 @@ export default async function map(treelike, operation) {
25
26
  // The tree in which the map operation happens
26
27
  const context = this;
27
28
 
29
+ if (isUnpackable(operation)) {
30
+ operation = await operation.unpack();
31
+ }
28
32
  const options = extendedOptions(context, operation);
33
+
29
34
  const mapped = mapTransform(source, options);
30
35
  mapped.parent = context;
31
36
  return mapped;
@@ -66,8 +71,8 @@ function extendedOptions(context, operation) {
66
71
 
67
72
  const { deep, extension, needsSourceValue } = options;
68
73
  const description = options.description ?? `map ${extension ?? ""}`;
69
- const keyFn = options.key;
70
- const inverseKeyFn = options.inverseKey;
74
+ let keyFn = options.key;
75
+ let inverseKeyFn = options.inverseKey;
71
76
 
72
77
  if (extension && (keyFn || inverseKeyFn)) {
73
78
  throw new TypeError(
@@ -75,47 +80,60 @@ function extendedOptions(context, operation) {
75
80
  );
76
81
  }
77
82
 
78
- let extendedValueFn;
79
83
  if (valueFn) {
80
- const resolvedValueFn = toFunction(valueFn);
81
- // Have the value function run in this tree.
82
- extendedValueFn = resolvedValueFn.bind(context);
84
+ // @ts-ignore
85
+ valueFn = toFunction(valueFn);
86
+ // By default, run the value function in the context of this tree so that
87
+ // Origami builtins can be used as value functions.
88
+ // @ts-ignore
89
+ const bound = valueFn.bind(context);
90
+ // @ts-ignore
91
+ Object.assign(bound, valueFn);
92
+ valueFn = bound;
83
93
  }
84
94
 
85
- // Extend the key functions to run in this tree.
86
- let extendedKeyFn;
87
- let extendedInverseKeyFn;
88
95
  if (extension) {
96
+ // Generate key/inverseKey functions from the extension
89
97
  let { resultExtension, sourceExtension } = parseExtensions(extension);
90
98
  const keyFns = keyFunctionsForExtensions({
91
99
  resultExtension,
92
100
  sourceExtension,
93
101
  });
94
- extendedKeyFn = keyFns.key;
95
- extendedInverseKeyFn = keyFns.inverseKey;
102
+ keyFn = keyFns.key;
103
+ inverseKeyFn = keyFns.inverseKey;
96
104
  } else if (keyFn) {
97
- const resolvedKeyFn = toFunction(keyFn);
98
- async function keyWithValueFn(sourceKey, sourceTree) {
99
- const sourceValue = await sourceTree.get(sourceKey);
100
- const resultKey = await resolvedKeyFn(sourceValue, sourceKey, sourceTree);
101
- return resultKey;
102
- }
103
- const keyFns = cachedKeyFunctions(keyWithValueFn, deep);
104
- extendedKeyFn = keyFns.key;
105
- extendedInverseKeyFn = keyFns.inverseKey;
105
+ // Extend the key function to include a value parameter
106
+ keyFn = extendKeyFn(keyFn);
106
107
  } else {
107
- // Use sidecar keyFn/inverseKey functions if the valueFn defines them.
108
- extendedKeyFn = /** @type {any} */ (valueFn)?.key;
109
- extendedInverseKeyFn = /** @type {any} */ (valueFn)?.inverseKey;
108
+ // Use sidecar key/inverseKey functions if the valueFn defines them
109
+ keyFn = /** @type {any} */ (valueFn)?.key;
110
+ inverseKeyFn = /** @type {any} */ (valueFn)?.inverseKey;
111
+ }
112
+
113
+ if (keyFn && !inverseKeyFn) {
114
+ // Only keyFn was provided, so we need to generate the inverseKeyFn
115
+ const keyFns = cachedKeyFunctions(keyFn, deep);
116
+ keyFn = keyFns.key;
117
+ inverseKeyFn = keyFns.inverseKey;
110
118
  }
111
119
 
112
120
  return {
113
121
  deep,
114
122
  description,
115
- inverseKey: extendedInverseKeyFn,
116
- key: extendedKeyFn,
123
+ inverseKey: inverseKeyFn,
124
+ key: keyFn,
117
125
  needsSourceValue,
118
- value: extendedValueFn,
126
+ value: valueFn,
127
+ };
128
+ }
129
+
130
+ // Extend the key function to include a value parameter
131
+ function extendKeyFn(keyFn) {
132
+ keyFn = toFunction(keyFn);
133
+ return async function keyWithValueFn(sourceKey, sourceTree) {
134
+ const sourceValue = await sourceTree.get(sourceKey);
135
+ const resultKey = await keyFn(sourceValue, sourceKey, sourceTree);
136
+ return resultKey;
119
137
  };
120
138
  }
121
139
 
@@ -1,115 +0,0 @@
1
- const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
2
-
3
- const mapStringsToModifications = new Map();
4
-
5
- /**
6
- * Normalize indentation in a tagged template string.
7
- *
8
- * @param {TemplateStringsArray} strings
9
- * @param {...any} values
10
- * @returns {string}
11
- */
12
- export default function indent(strings, ...values) {
13
- let modified = mapStringsToModifications.get(strings);
14
- if (!modified) {
15
- modified = modifyStrings(strings);
16
- mapStringsToModifications.set(strings, modified);
17
- }
18
- const { blockIndentations, strings: modifiedStrings } = modified;
19
- return joinBlocks(modifiedStrings, values, blockIndentations);
20
- }
21
-
22
- // Join strings and values, applying the given block indentation to the lines of
23
- // values for block placholders.
24
- function joinBlocks(strings, values, blockIndentations) {
25
- let result = strings[0];
26
- for (let i = 0; i < values.length; i++) {
27
- let text = values[i];
28
- if (text) {
29
- const blockIndentation = blockIndentations[i];
30
- if (blockIndentation) {
31
- const lines = text.split("\n");
32
- text = "";
33
- if (lines.at(-1) === "") {
34
- // Drop empty last line
35
- lines.pop();
36
- }
37
- for (let line of lines) {
38
- text += blockIndentation + line + "\n";
39
- }
40
- }
41
- result += text;
42
- }
43
- result += strings[i + 1];
44
- }
45
- return result;
46
- }
47
-
48
- // Given an array of template boilerplate strings, return an object { modified,
49
- // blockIndentations } where `strings` is the array of strings with indentation
50
- // removed, and `blockIndentations` is an array of indentation strings for each
51
- // block placeholder.
52
- function modifyStrings(strings) {
53
- // Phase one: Identify the indentation based on the first real line of the
54
- // first string (skipping the initial newline), and remove this indentation
55
- // from all lines of all strings.
56
- let indent;
57
- if (strings.length > 0 && strings[0].startsWith("\n")) {
58
- // Look for indenttation
59
- const firstLineWhitespaceRegex = /^\n(?<indent>[ \t]*)/;
60
- const match = strings[0].match(firstLineWhitespaceRegex);
61
- indent = match?.groups.indent;
62
- }
63
-
64
- // Determine the modified strings. If this invoked as a JS tagged template
65
- // literal, the `strings` argument will be an odd array-ish object that we'll
66
- // want to convert to a real array.
67
- let modified;
68
- if (indent) {
69
- // De-indent the strings.
70
- const indentationRegex = new RegExp(`\n${indent}`, "g");
71
- // The `replaceAll` also converts strings to a real array.
72
- modified = strings.map((string) =>
73
- string.replaceAll(indentationRegex, "\n")
74
- );
75
- // Remove indentation from last line of last string
76
- modified[modified.length - 1] = modified
77
- .at(-1)
78
- .replace(lastLineWhitespaceRegex, "\n");
79
- } else {
80
- // No indentation; just copy the strings so we have a real array
81
- modified = strings.slice();
82
- }
83
-
84
- // Phase two: Identify any block placholders, identify and remove their
85
- // preceding indentation, and remove the following newline. Work backward from
86
- // the end towards the start because we're modifying the strings in place and
87
- // our pattern matching won't work going forward from start to end.
88
- let blockIndentations = [];
89
- for (let i = modified.length - 2; i >= 0; i--) {
90
- // Get the modified before and after substitution with index `i`
91
- const beforeString = modified[i];
92
- const afterString = modified[i + 1];
93
- const match = beforeString.match(lastLineWhitespaceRegex);
94
- if (match && afterString.startsWith("\n")) {
95
- // The substitution between these strings is a block substitution
96
- let blockIndentation = match.groups.indent;
97
- blockIndentations[i] = blockIndentation;
98
- // Trim the before and after strings
99
- if (blockIndentation) {
100
- modified[i] = beforeString.slice(0, -blockIndentation.length);
101
- }
102
- modified[i + 1] = afterString.slice(1);
103
- }
104
- }
105
-
106
- // Remove newline from start of first string *after* removing indentation.
107
- if (modified[0].startsWith("\n")) {
108
- modified[0] = modified[0].slice(1);
109
- }
110
-
111
- return {
112
- blockIndentations,
113
- strings: modified,
114
- };
115
- }