@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/CommentStyle.js +104 -0
- package/LICENSE +21 -0
- package/ManagedSection.js +211 -0
- package/Section.js +126 -0
- package/SectionDialect.js +164 -0
- package/SectionDocument.js +197 -0
- package/SectionOutcome.js +20 -0
- package/index.d.ts +812 -0
- package/index.js +8 -0
- package/internal/reconcile.js +113 -0
- package/internal/scan.js +135 -0
- package/package.json +45 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -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 };
|