@effected/templates 0.1.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/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { CommentStyle } from "./CommentStyle.js";
2
+ import { SectionDialect, SectionRenderError } from "./SectionDialect.js";
3
+ import { CheckOutcome, SyncOutcome } from "./SectionOutcome.js";
4
+ import { PlacedSection, Section, SectionId, SectionKey } from "./Section.js";
5
+ import { SectionDocument, SectionParseError } from "./SectionDocument.js";
6
+ import { ManagedSection, SectionFileError } from "./ManagedSection.js";
7
+
8
+ export { CheckOutcome, CommentStyle, ManagedSection, PlacedSection, Section, SectionDialect, SectionDocument, SectionFileError, SectionId, SectionKey, SectionParseError, SectionRenderError, SyncOutcome };
@@ -0,0 +1,113 @@
1
+ import { SectionRenderError } from "../SectionDialect.js";
2
+ import { SyncOutcome } from "../SectionOutcome.js";
3
+ import { identityOf } from "./scan.js";
4
+ import { Equal, Result } from "effect";
5
+
6
+ //#region src/internal/reconcile.ts
7
+ const reconcile = (input) => {
8
+ const { text, placed, declared, dialect, eol } = input;
9
+ const rendered = /* @__PURE__ */ new Map();
10
+ for (const section of declared) {
11
+ const identity = identityOf(section.key, section.commentStyle);
12
+ if (rendered.has(identity)) return Result.fail(new SectionRenderError({
13
+ reason: "duplicateDeclaration",
14
+ key: section.key
15
+ }));
16
+ const result = dialect.render(section, eol);
17
+ if (Result.isFailure(result)) return Result.fail(result.failure);
18
+ rendered.set(identity, result.success);
19
+ }
20
+ const onDisk = /* @__PURE__ */ new Map();
21
+ for (const entry of placed) onDisk.set(identityOf(entry.section.key, entry.section.commentStyle), entry);
22
+ const outcomes = declared.map((section) => {
23
+ const current = onDisk.get(identityOf(section.key, section.commentStyle));
24
+ if (current === void 0) return SyncOutcome.Created({ section });
25
+ return Equal.equals(current.section, section) ? SyncOutcome.Unchanged({ section }) : SyncOutcome.Updated({
26
+ before: current.section,
27
+ after: section
28
+ });
29
+ });
30
+ const items = [];
31
+ let cursor = 0;
32
+ for (const entry of placed) {
33
+ items.push({
34
+ kind: "text",
35
+ value: text.slice(cursor, entry.start)
36
+ });
37
+ items.push({
38
+ kind: "section",
39
+ identity: identityOf(entry.section.key, entry.section.commentStyle),
40
+ raw: text.slice(entry.start, entry.end),
41
+ render: void 0
42
+ });
43
+ cursor = entry.end;
44
+ }
45
+ items.push({
46
+ kind: "text",
47
+ value: text.slice(cursor)
48
+ });
49
+ const targets = new Set(declared.map((section) => identityOf(section.key, section.commentStyle)));
50
+ const slots = [];
51
+ items.forEach((item, index) => {
52
+ if (item.kind === "section" && targets.has(item.identity)) slots.push(index);
53
+ });
54
+ const itemIndexByDeclared = /* @__PURE__ */ new Map();
55
+ let slotCursor = 0;
56
+ declared.forEach((section, declaredIndex) => {
57
+ const identity = identityOf(section.key, section.commentStyle);
58
+ if (!onDisk.has(identity) || slotCursor >= slots.length) return;
59
+ const itemIndex = slots[slotCursor] ?? 0;
60
+ const item = items[itemIndex];
61
+ if (item?.kind === "section") item.render = rendered.get(identity);
62
+ itemIndexByDeclared.set(declaredIndex, itemIndex);
63
+ slotCursor += 1;
64
+ });
65
+ const beforeAnchor = /* @__PURE__ */ new Map();
66
+ const afterAnchor = /* @__PURE__ */ new Map();
67
+ const appended = [];
68
+ const pushInto = (map, anchor, value) => {
69
+ const list = map.get(anchor) ?? [];
70
+ list.push(value);
71
+ map.set(anchor, list);
72
+ };
73
+ declared.forEach((section, declaredIndex) => {
74
+ const identity = identityOf(section.key, section.commentStyle);
75
+ if (onDisk.has(identity)) return;
76
+ const block = rendered.get(identity) ?? "";
77
+ for (let next = declaredIndex + 1; next < declared.length; next += 1) {
78
+ const anchor = itemIndexByDeclared.get(next);
79
+ if (anchor !== void 0) {
80
+ pushInto(beforeAnchor, anchor, block);
81
+ return;
82
+ }
83
+ }
84
+ for (let previous = declaredIndex - 1; previous >= 0; previous -= 1) {
85
+ const anchor = itemIndexByDeclared.get(previous);
86
+ if (anchor !== void 0) {
87
+ pushInto(afterAnchor, anchor, block);
88
+ return;
89
+ }
90
+ }
91
+ appended.push(block);
92
+ });
93
+ const parts = [];
94
+ items.forEach((item, index) => {
95
+ if (item.kind === "text") {
96
+ parts.push(item.value);
97
+ return;
98
+ }
99
+ for (const block of beforeAnchor.get(index) ?? []) parts.push(block, eol, eol);
100
+ parts.push(item.render ?? item.raw);
101
+ for (const block of afterAnchor.get(index) ?? []) parts.push(eol, eol, block);
102
+ });
103
+ let output = parts.join("");
104
+ for (const block of appended) output = output.trim() === "" ? `${block}${eol}` : `${output.replace(/(?:\r?\n)+$/, "")}${eol}${eol}${block}${eol}`;
105
+ return Result.succeed({
106
+ text: output,
107
+ outcomes,
108
+ changed: output !== text
109
+ });
110
+ };
111
+
112
+ //#endregion
113
+ export { reconcile };
@@ -0,0 +1,135 @@
1
+ import { PlacedSection, Section } from "../Section.js";
2
+
3
+ //#region src/internal/scan.ts
4
+ /** The ways a document can be structurally unreadable. */
5
+ const SCAN_FAILURE_REASONS = [
6
+ "unterminatedSection",
7
+ "orphanedEnd",
8
+ "overlappingSections",
9
+ "duplicateSection"
10
+ ];
11
+ /** A document's dominant line ending. Any CRLF makes the document CRLF. */
12
+ const detectEol = (text) => text.includes("\r\n") ? "\r\n" : "\n";
13
+ /**
14
+ * Collapse CRLF to LF.
15
+ *
16
+ * @remarks
17
+ * Applied to section content at parse time so that the `Section` values the
18
+ * rest of the package compares are already canonical. Doing it here rather
19
+ * than inside equality keeps `Equal.equals` honest for a consumer comparing
20
+ * two sections directly, and it is what stops a CRLF document from reporting
21
+ * drift on every single run.
22
+ */
23
+ const normalizeEol = (text) => text.replace(/\r\n/g, "\n");
24
+ /** Strip exactly one line break from the start of a string. */
25
+ const stripLeadingBreak = (text) => text.startsWith("\r\n") ? text.slice(2) : text.startsWith("\n") ? text.slice(1) : text;
26
+ /** Strip exactly one line break from the end of a string. */
27
+ const stripTrailingBreak = (text) => text.endsWith("\r\n") ? text.slice(0, -2) : text.endsWith("\n") ? text.slice(0, -1) : text;
28
+ /** Offsets at which each 1-based line starts, for O(log n) line lookup. */
29
+ const lineStarts = (text) => {
30
+ const starts = [0];
31
+ for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) === 10) starts.push(index + 1);
32
+ return starts;
33
+ };
34
+ const lineAt = (starts, offset) => {
35
+ let low = 0;
36
+ let high = starts.length - 1;
37
+ while (low < high) {
38
+ const mid = low + high + 1 >> 1;
39
+ if ((starts[mid] ?? 0) <= offset) low = mid;
40
+ else high = mid - 1;
41
+ }
42
+ return low + 1;
43
+ };
44
+ /** The identity two sections must share to be duplicates of each other. */
45
+ const identityOf = (key, style) => `${key}\u0000${style.id}`;
46
+ const collectHits = (text, dialect) => {
47
+ const hits = [];
48
+ const seen = /* @__PURE__ */ new Set();
49
+ for (const matcher of dialect.matchers()) for (const match of text.matchAll(matcher.regex)) {
50
+ const start = match.index;
51
+ if (start === void 0 || seen.has(start)) continue;
52
+ seen.add(start);
53
+ hits.push({
54
+ kind: match[1] === "BEGIN" ? "BEGIN" : "END",
55
+ key: match[2] ?? "",
56
+ style: matcher.style,
57
+ start,
58
+ end: start + match[0].length
59
+ });
60
+ }
61
+ return hits.sort((left, right) => left.start - right.start);
62
+ };
63
+ /**
64
+ * Locate every managed section, in document order.
65
+ *
66
+ * @remarks
67
+ * A bounded linear pass — no recursion, so no stack-overflow surface on
68
+ * hostile input. Sections cannot nest: a begin marker encountered while
69
+ * another section is open is `overlappingSections`, not an inner block.
70
+ */
71
+ const scan = (text, dialect) => {
72
+ const starts = lineStarts(text);
73
+ const sections = [];
74
+ const firstSeenAt = /* @__PURE__ */ new Map();
75
+ let open;
76
+ for (const hit of collectHits(text, dialect)) {
77
+ if (hit.kind === "BEGIN") {
78
+ if (open !== void 0) return {
79
+ ok: false,
80
+ failure: {
81
+ reason: "overlappingSections",
82
+ line: lineAt(starts, hit.start),
83
+ key: hit.key
84
+ }
85
+ };
86
+ open = hit;
87
+ continue;
88
+ }
89
+ if (open === void 0 || open.key !== hit.key || open.style.id !== hit.style.id) return {
90
+ ok: false,
91
+ failure: {
92
+ reason: "orphanedEnd",
93
+ line: lineAt(starts, hit.start),
94
+ key: hit.key
95
+ }
96
+ };
97
+ const identity = identityOf(open.key, open.style);
98
+ if (firstSeenAt.has(identity)) return {
99
+ ok: false,
100
+ failure: {
101
+ reason: "duplicateSection",
102
+ line: lineAt(starts, open.start),
103
+ key: open.key
104
+ }
105
+ };
106
+ firstSeenAt.set(identity, open.start);
107
+ const inner = stripTrailingBreak(stripLeadingBreak(text.slice(open.end, hit.start)));
108
+ sections.push(PlacedSection.make({
109
+ section: Section.make({
110
+ key: open.key,
111
+ commentStyle: open.style,
112
+ content: normalizeEol(inner)
113
+ }),
114
+ start: open.start,
115
+ end: hit.end,
116
+ line: lineAt(starts, open.start)
117
+ }));
118
+ open = void 0;
119
+ }
120
+ if (open !== void 0) return {
121
+ ok: false,
122
+ failure: {
123
+ reason: "unterminatedSection",
124
+ line: lineAt(starts, open.start),
125
+ key: open.key
126
+ }
127
+ };
128
+ return {
129
+ ok: true,
130
+ sections
131
+ };
132
+ };
133
+
134
+ //#endregion
135
+ export { SCAN_FAILURE_REASONS, detectEol, identityOf, normalizeEol, scan };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@effected/templates",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Managed-section blocks in user-editable files: parse, reconcile, sync and check delimited regions.",
6
+ "keywords": [
7
+ "templates",
8
+ "managed-section",
9
+ "codegen",
10
+ "sync",
11
+ "effect",
12
+ "effected"
13
+ ],
14
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/templates#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/spencerbeggs/effected/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/spencerbeggs/effected.git",
21
+ "directory": "packages/templates"
22
+ },
23
+ "license": "MIT",
24
+ "author": {
25
+ "name": "C. Spencer Beggs",
26
+ "email": "spencer@beggs.codes",
27
+ "url": "https://spencerbeg.gs"
28
+ },
29
+ "sideEffects": false,
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "import": "./index.js",
35
+ "default": "./index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "peerDependencies": {
40
+ "effect": "4.0.0-beta.101"
41
+ },
42
+ "engines": {
43
+ "node": ">=24.11.0"
44
+ }
45
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.12"
9
+ }
10
+ ]
11
+ }