@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.
@@ -0,0 +1,197 @@
1
+ import { SectionDialect } from "./SectionDialect.js";
2
+ import { CheckOutcome } from "./SectionOutcome.js";
3
+ import { PlacedSection } from "./Section.js";
4
+ import { SCAN_FAILURE_REASONS, detectEol, identityOf, normalizeEol, scan } from "./internal/scan.js";
5
+ import { reconcile } from "./internal/reconcile.js";
6
+ import { Effect, Equal, Option, Result, Schema } from "effect";
7
+
8
+ //#region src/SectionDocument.ts
9
+ /**
10
+ * Raised when a document's managed-section structure cannot be read without
11
+ * guessing.
12
+ *
13
+ * @remarks
14
+ * Every reason names an ambiguity whose silent resolution corrupts a file.
15
+ * A skipped unterminated marker makes the next write append a **second** copy
16
+ * of the section; a duplicate means every sync updates the first copy while
17
+ * the stale second one lives on disk forever. Malformed input therefore fails
18
+ * through the typed channel with the line that identifies it, and the user
19
+ * fixes their file.
20
+ *
21
+ * @public
22
+ */
23
+ var SectionParseError = class SectionParseError extends Schema.TaggedErrorClass()("SectionParseError", {
24
+ /** Which ambiguity was found. */
25
+ reason: Schema.Literals(SCAN_FAILURE_REASONS),
26
+ /** 1-based line of the offending marker. */
27
+ line: Schema.Number,
28
+ /** The section key involved, when the failure names one. */
29
+ key: Schema.optionalKey(Schema.String),
30
+ /** The file the document came from. Absent for a document parsed from a string. */
31
+ path: Schema.optionalKey(Schema.String)
32
+ }) {
33
+ get message() {
34
+ const where = this.path === void 0 ? `line ${this.line}` : `${this.path}:${this.line}`;
35
+ const which = this.key === void 0 ? "" : ` for section "${this.key}"`;
36
+ return `${REASON_PROSE[this.reason]}${which} at ${where}`;
37
+ }
38
+ /**
39
+ * The same failure, attributed to a file.
40
+ *
41
+ * @remarks
42
+ * The pure core has no path to report; the service that read the file
43
+ * attaches one on the way out.
44
+ */
45
+ static at(path, error) {
46
+ return SectionParseError.make({
47
+ reason: error.reason,
48
+ line: error.line,
49
+ ...error.key === void 0 ? {} : { key: error.key },
50
+ path
51
+ });
52
+ }
53
+ };
54
+ const REASON_PROSE = {
55
+ unterminatedSection: "Managed section is never closed",
56
+ orphanedEnd: "End marker closes no open section",
57
+ overlappingSections: "Managed section opens inside another",
58
+ duplicateSection: "Managed section appears twice"
59
+ };
60
+ /**
61
+ * A parsed document: its text, the dialect it was read with, and every
62
+ * managed section found in it.
63
+ *
64
+ * @remarks
65
+ * This is the package's pure core. Parsing, inspection, reconciliation and
66
+ * removal are all string-to-string, so every interesting invariant —
67
+ * idempotency, text preservation, ordering normalization, line-ending
68
+ * handling, marker-injection refusal — is assertable from a string literal
69
+ * with no layer, no runtime and no filesystem.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { CommentStyle, SectionDocument, SectionId } from "@effected/templates";
74
+ * import { Result } from "effect";
75
+ *
76
+ * const doc = SectionDocument.parseResult(source);
77
+ * if (Result.isSuccess(doc)) {
78
+ * const id = SectionId.make({ key: "example-tool", commentStyle: CommentStyle.hash });
79
+ * const next = doc.success.reconcile([id.section("echo hello")]);
80
+ * }
81
+ * ```
82
+ *
83
+ * @public
84
+ */
85
+ var SectionDocument = class SectionDocument extends Schema.Class("SectionDocument")({
86
+ /** The document's source text, exactly as parsed. */
87
+ text: Schema.String,
88
+ /** The marker vocabulary this document was read with. */
89
+ dialect: SectionDialect,
90
+ /** Every managed section found, in document order. */
91
+ sections: Schema.Array(PlacedSection),
92
+ /** The document's dominant line ending. */
93
+ eol: Schema.Literals(["\n", "\r\n"])
94
+ }) {
95
+ /**
96
+ * Parse a document. The synchronous primitive.
97
+ *
98
+ * @remarks
99
+ * Pure computation exposes the sync form as the primitive; {@link SectionDocument.parse}
100
+ * derives from this and adds only the tracing span, so the two cannot drift.
101
+ * Synchronous callers — a lint hook, a build plugin — use this directly and
102
+ * never build an Effect runtime.
103
+ */
104
+ static parseResult(text, dialect = SectionDialect.default) {
105
+ const scanned = scan(text, dialect);
106
+ if (!scanned.ok) {
107
+ const { reason, line, key } = scanned.failure;
108
+ return Result.fail(SectionParseError.make({
109
+ reason,
110
+ line,
111
+ ...key === void 0 ? {} : { key }
112
+ }));
113
+ }
114
+ return Result.succeed(SectionDocument.make({
115
+ text,
116
+ dialect,
117
+ sections: scanned.sections,
118
+ eol: detectEol(text)
119
+ }));
120
+ }
121
+ /**
122
+ * Parse a document, in `Effect`.
123
+ *
124
+ * @remarks
125
+ * Defined in terms of {@link SectionDocument.parseResult} — synchronous
126
+ * callers can use that variant directly.
127
+ */
128
+ static parse = Effect.fn("SectionDocument.parse")((text, dialect = SectionDialect.default) => Effect.fromResult(SectionDocument.parseResult(text, dialect)));
129
+ /** The section with this identity, if the document has one. */
130
+ read(id) {
131
+ const identity = identityOf(id.key, id.commentStyle);
132
+ const found = this.sections.find((placed) => identityOf(placed.section.key, placed.section.commentStyle) === identity);
133
+ return found === void 0 ? Option.none() : Option.some(found.section);
134
+ }
135
+ /** Whether the document carries a section with this identity. */
136
+ has(id) {
137
+ return Option.isSome(this.read(id));
138
+ }
139
+ /**
140
+ * Compare a declared section against the document, changing nothing.
141
+ *
142
+ * @remarks
143
+ * Total: there is no way for a comparison to fail. Line endings are
144
+ * normalized on both sides, so a CRLF document does not report drift
145
+ * against LF content forever.
146
+ */
147
+ check(section) {
148
+ const expected = section.withContent(normalizeEol(section.content));
149
+ const current = this.read(section.id);
150
+ if (Option.isNone(current)) return CheckOutcome.Absent({ id: section.id });
151
+ return Equal.equals(current.value, expected) ? CheckOutcome.UpToDate({ section: current.value }) : CheckOutcome.Drifted({
152
+ onDisk: current.value,
153
+ expected
154
+ });
155
+ }
156
+ /**
157
+ * Fit a declared set of sections into this document.
158
+ *
159
+ * @remarks
160
+ * Declared sections come back **in declared order** — the ordering
161
+ * normalization is a guarantee consumers depend on, not a side effect.
162
+ * Text outside a managed span and sections this dialect does not own are
163
+ * preserved byte-for-byte. Nothing is rendered into the output until every
164
+ * declared section has rendered successfully, so a refusal leaves the
165
+ * document untouched.
166
+ */
167
+ reconcile(sections) {
168
+ return reconcile({
169
+ text: this.text,
170
+ placed: this.sections,
171
+ declared: sections.map((section) => section.withContent(normalizeEol(section.content))),
172
+ dialect: this.dialect,
173
+ eol: this.eol
174
+ });
175
+ }
176
+ /**
177
+ * The document with this section removed, or `none` if it was not there.
178
+ *
179
+ * @remarks
180
+ * The blank lines around the removed block collapse into a single
181
+ * separator, so repeated removals never accumulate gaps.
182
+ */
183
+ remove(id) {
184
+ const identity = identityOf(id.key, id.commentStyle);
185
+ const found = this.sections.find((placed) => identityOf(placed.section.key, placed.section.commentStyle) === identity);
186
+ if (found === void 0) return Option.none();
187
+ const eol = this.eol;
188
+ const before = this.text.slice(0, found.start).replace(/(?:\r?\n)+$/, "");
189
+ const after = this.text.slice(found.end).replace(/^(?:\r?\n)+/, "");
190
+ if (before !== "" && after !== "") return Option.some(`${before}${eol}${eol}${after}`);
191
+ if (before !== "") return Option.some(`${before}${eol}`);
192
+ return Option.some(after);
193
+ }
194
+ };
195
+
196
+ //#endregion
197
+ export { SectionDocument, SectionParseError };
@@ -0,0 +1,20 @@
1
+ import { Data } from "effect";
2
+
3
+ //#region src/SectionOutcome.ts
4
+ /**
5
+ * Constructors and matchers for `SyncOutcome` — e.g.
6
+ * `SyncOutcome.Created({ section })`, `SyncOutcome.$is("Updated")`.
7
+ *
8
+ * @public
9
+ */
10
+ const SyncOutcome = Data.taggedEnum();
11
+ /**
12
+ * Constructors and matchers for `CheckOutcome` — e.g.
13
+ * `CheckOutcome.Absent({ id })`, `CheckOutcome.$is("Drifted")`.
14
+ *
15
+ * @public
16
+ */
17
+ const CheckOutcome = Data.taggedEnum();
18
+
19
+ //#endregion
20
+ export { CheckOutcome, SyncOutcome };