@effected/templates 0.3.0 → 0.5.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/README.md +29 -0
- package/Section.js +33 -7
- package/SectionDialect.js +28 -7
- package/index.d.ts +50 -20
- package/internal/attributes.js +63 -0
- package/internal/scan.js +12 -2
- package/package.json +2 -2
- package/tsdoc-metadata.json +1 -1
package/README.md
CHANGED
|
@@ -65,11 +65,40 @@ Effect.runPromise(program.pipe(Effect.provide(ManagedSection.layer), Effect.prov
|
|
|
65
65
|
// example output on a second, identical run: [ "Unchanged", "Unchanged" ]
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Marker attributes
|
|
69
|
+
|
|
70
|
+
A `BEGIN` marker can carry `name="value"` pairs, so a tool reads a block's provenance — which run wrote it, and when — off the marker line without opening the block:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { CommentStyle, SectionDocument, SectionId } from "@effected/templates";
|
|
74
|
+
import { Result } from "effect";
|
|
75
|
+
|
|
76
|
+
const Tool = SectionId.make({ key: "tool", commentStyle: CommentStyle.hash });
|
|
77
|
+
const doc = SectionDocument.parseResult("#!/usr/bin/env sh\n");
|
|
78
|
+
|
|
79
|
+
if (Result.isSuccess(doc)) {
|
|
80
|
+
const next = doc.success.reconcile([Tool.section("npx tool run", { origin: "ci", runId: "1873" })]);
|
|
81
|
+
console.log(Result.isSuccess(next) ? next.success.text : next.failure.message);
|
|
82
|
+
// #!/usr/bin/env sh
|
|
83
|
+
//
|
|
84
|
+
// # --- BEGIN tool MANAGED SECTION origin="ci" runId="1873" ---
|
|
85
|
+
// npx tool run
|
|
86
|
+
// # --- END tool MANAGED SECTION ---
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Attributes are metadata, never identity: a section is found by key and comment style alone, so bumping `runId` updates that block in place instead of orphaning it and appending a second one. They do count as difference — `check` answers `Drifted` when a declared attribute disagrees with the marker on disk, which is what lets a tool rewrite a block whose only change is its metadata. Omitting the argument and passing `{}` are the same section, so a caller that never uses attributes sees no drift against a bare marker.
|
|
91
|
+
|
|
92
|
+
The grammar has no escaping, deliberately: names match `[A-Za-z][A-Za-z0-9_-]*`, values are double-quoted and carry neither `"` nor a line break, and anything outside that fails typed as a `SectionRenderError` with `reason: "invalidAttribute"` naming the attribute — before a byte is written. An `END` marker never carries attributes, and an attribute run that does not parse cleanly (a mangled pair, one name declared twice) makes the whole line ordinary content rather than a marker.
|
|
93
|
+
|
|
94
|
+
Attributed markers are a one-way compatibility break: a scanner predating this feature does not recognize one at all, so the block it opens stops being a managed section for that tool. Writing attributes into a file another tool also manages is a decision, not a detail.
|
|
95
|
+
|
|
68
96
|
## Features
|
|
69
97
|
|
|
70
98
|
- `SectionDocument` — the pure core: `parse` / `read` / `has` / `check` / `reconcile` / `remove` over a plain string, with no `Effect`, no IO and no runtime needed to test it.
|
|
71
99
|
- `ManagedSection` — the service: `read`, `readAll`, `isManaged`, `sync`, `syncAll`, `check`, `checkAll` and `remove`, each writing back only when the text changed.
|
|
72
100
|
- `CommentStyle` — an open set of `{prefix, suffix?}` presets (`hash`, `slash`, `semicolon`, `dash`, `html`, `block`) covering both line comments and wrapped ones like `<!-- … -->`, so a managed section can live in Markdown or HTML, not just source files.
|
|
101
|
+
- Marker attributes — `name="value"` pairs on a `BEGIN` marker, via `SectionId.section(content, attributes)` and `Section.attributes`: metadata for the tool, never part of a section's identity.
|
|
73
102
|
- `SyncOutcome` / `CheckOutcome` — tagged results (`Created` / `Updated` / `Unchanged`, `Absent` / `UpToDate` / `Drifted`) that carry the sections involved rather than a diff, so a caller renders exactly the comparison it needs.
|
|
74
103
|
- Byte-preserving outside managed blocks: every character outside a declared section's span survives a sync, in order, including a leading BOM.
|
|
75
104
|
- Idempotent by construction: syncing an already-up-to-date document writes nothing and touches no mtime.
|
package/Section.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommentStyle } from "./CommentStyle.js";
|
|
2
|
-
import { Schema } from "effect";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
//#region src/Section.ts
|
|
5
5
|
/**
|
|
@@ -52,12 +52,20 @@ var SectionId = class extends Schema.Class("SectionId")({
|
|
|
52
52
|
/** How this section's markers are commented out. */
|
|
53
53
|
commentStyle: CommentStyle
|
|
54
54
|
}) {
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Pair this identity with the content a tool wants inside it.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* `attributes` become `name="value"` pairs on the section's BEGIN marker,
|
|
60
|
+
* emitted in the record's insertion order. They are metadata, not identity:
|
|
61
|
+
* see {@link Section} for the grammar and the equality rules.
|
|
62
|
+
*/
|
|
63
|
+
section(content, attributes) {
|
|
57
64
|
return Section.make({
|
|
58
65
|
key: this.key,
|
|
59
66
|
commentStyle: this.commentStyle,
|
|
60
|
-
content
|
|
67
|
+
content,
|
|
68
|
+
...attributes === void 0 ? {} : { attributes }
|
|
61
69
|
});
|
|
62
70
|
}
|
|
63
71
|
};
|
|
@@ -74,6 +82,21 @@ var SectionId = class extends Schema.Class("SectionId")({
|
|
|
74
82
|
* parse time rather than inside equality, so `Equal.equals` stays honest for
|
|
75
83
|
* a consumer comparing two sections directly.
|
|
76
84
|
*
|
|
85
|
+
* `attributes` are `name="value"` pairs carried on the BEGIN marker — metadata
|
|
86
|
+
* a tool wants readable from the marker line itself, without opening the
|
|
87
|
+
* content. They **participate in equality** (an attribute change is real
|
|
88
|
+
* drift) but **never in identity**: the section a marker names is decided by
|
|
89
|
+
* key and comment style alone, so changing an attribute updates a block in
|
|
90
|
+
* place rather than orphaning it. Equality over attributes is record equality
|
|
91
|
+
* — order-insensitive, so two sections carrying the same pairs in different
|
|
92
|
+
* insertion orders compare equal — while an actual rewrite renders the pairs
|
|
93
|
+
* in the record's insertion order. Omitting the field and passing `{}` are
|
|
94
|
+
* the same section: the constructor defaults to an empty record.
|
|
95
|
+
*
|
|
96
|
+
* Attribute names must match `[A-Za-z][A-Za-z0-9_-]*` and values may not
|
|
97
|
+
* contain `"` or a line break; violations fail typed at render, not here, so
|
|
98
|
+
* runtime data can never turn construction into a defect.
|
|
99
|
+
*
|
|
77
100
|
* @public
|
|
78
101
|
*/
|
|
79
102
|
var Section = class Section extends Schema.Class("Section")({
|
|
@@ -82,7 +105,9 @@ var Section = class Section extends Schema.Class("Section")({
|
|
|
82
105
|
/** How this section's markers are commented out. */
|
|
83
106
|
commentStyle: CommentStyle,
|
|
84
107
|
/** Everything between the markers, exclusive of the boundary line breaks. */
|
|
85
|
-
content: Schema.String
|
|
108
|
+
content: Schema.String,
|
|
109
|
+
/** The BEGIN marker's `name="value"` pairs. Empty when the marker carries none. */
|
|
110
|
+
attributes: Schema.Record(Schema.String, Schema.String).pipe(Schema.withConstructorDefault(Effect.succeed({})))
|
|
86
111
|
}) {
|
|
87
112
|
/** This section's identity, without its content. */
|
|
88
113
|
get id() {
|
|
@@ -91,12 +116,13 @@ var Section = class Section extends Schema.Class("Section")({
|
|
|
91
116
|
commentStyle: this.commentStyle
|
|
92
117
|
});
|
|
93
118
|
}
|
|
94
|
-
/** The same section carrying different content. */
|
|
119
|
+
/** The same section carrying different content. Attributes are preserved. */
|
|
95
120
|
withContent(content) {
|
|
96
121
|
return Section.make({
|
|
97
122
|
key: this.key,
|
|
98
123
|
commentStyle: this.commentStyle,
|
|
99
|
-
content
|
|
124
|
+
content,
|
|
125
|
+
attributes: this.attributes
|
|
100
126
|
});
|
|
101
127
|
}
|
|
102
128
|
};
|
package/SectionDialect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CommentStyle } from "./CommentStyle.js";
|
|
2
|
+
import { ATTRIBUTE_NAME_PATTERN, isValidAttributeValue, parseAttributeRun } from "./internal/attributes.js";
|
|
2
3
|
import { Equal, Result, Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
//#region src/SectionDialect.ts
|
|
@@ -23,19 +24,27 @@ var SectionRenderError = class extends Schema.TaggedError()("SectionRenderError"
|
|
|
23
24
|
* declared twice in one call, so the caller stated two intentions for one
|
|
24
25
|
* block and any choice between them would be a guess; this is the
|
|
25
26
|
* caller-side twin of the document-side `duplicateSection`.
|
|
27
|
+
* `invalidAttribute` — an attribute's name is outside the
|
|
28
|
+
* `[A-Za-z][A-Za-z0-9_-]*` grammar, or its value contains `"` or a line
|
|
29
|
+
* break; either would render a marker the scanner could not read back
|
|
30
|
+
* verbatim, and there is no escaping mechanism by design.
|
|
26
31
|
*/
|
|
27
32
|
reason: Schema.Literals([
|
|
28
33
|
"markerInContent",
|
|
29
34
|
"unknownCommentStyle",
|
|
30
|
-
"duplicateDeclaration"
|
|
35
|
+
"duplicateDeclaration",
|
|
36
|
+
"invalidAttribute"
|
|
31
37
|
]),
|
|
32
38
|
/** The key of the section that could not be rendered. */
|
|
33
|
-
key: Schema.String
|
|
39
|
+
key: Schema.String,
|
|
40
|
+
/** The offending attribute's name, when the refusal names one. */
|
|
41
|
+
attribute: Schema.optionalKey(Schema.String)
|
|
34
42
|
}) {
|
|
35
43
|
get message() {
|
|
36
44
|
switch (this.reason) {
|
|
37
45
|
case "markerInContent": return `Section "${this.key}" has content containing a managed-section marker`;
|
|
38
46
|
case "unknownCommentStyle": return `Section "${this.key}" uses a comment style this dialect does not recognize`;
|
|
47
|
+
case "invalidAttribute": return this.attribute === void 0 ? `Section "${this.key}" declares an attribute with an invalid name or value` : `Section "${this.key}" declares attribute "${this.attribute}" with an invalid name or value`;
|
|
39
48
|
default: return `Section "${this.key}" was declared twice in one call`;
|
|
40
49
|
}
|
|
41
50
|
}
|
|
@@ -114,12 +123,18 @@ var SectionDialect = class SectionDialect extends Schema.Class("SectionDialect")
|
|
|
114
123
|
reason: "unknownCommentStyle",
|
|
115
124
|
key: section.key
|
|
116
125
|
}));
|
|
126
|
+
for (const [name, value] of Object.entries(section.attributes)) if (!ATTRIBUTE_NAME_PATTERN.test(name) || !isValidAttributeValue(value)) return Result.fail(new SectionRenderError({
|
|
127
|
+
reason: "invalidAttribute",
|
|
128
|
+
key: section.key,
|
|
129
|
+
attribute: name
|
|
130
|
+
}));
|
|
117
131
|
if (this.containsMarker(section.content)) return Result.fail(new SectionRenderError({
|
|
118
132
|
reason: "markerInContent",
|
|
119
133
|
key: section.key
|
|
120
134
|
}));
|
|
121
135
|
const body = eol === "\n" ? section.content : section.content.replace(/\n/g, eol);
|
|
122
|
-
|
|
136
|
+
const begin = this.marker("BEGIN", section.id, section.attributes);
|
|
137
|
+
return Result.succeed(`${begin}${eol}${body}${eol}${this.endMarker(section.id)}`);
|
|
123
138
|
}
|
|
124
139
|
/**
|
|
125
140
|
* True when `text` contains a line this dialect would read as a marker.
|
|
@@ -127,7 +142,12 @@ var SectionDialect = class SectionDialect extends Schema.Class("SectionDialect")
|
|
|
127
142
|
* @internal
|
|
128
143
|
*/
|
|
129
144
|
containsMarker(text) {
|
|
130
|
-
|
|
145
|
+
for (const matcher of this.matchers()) for (const match of text.matchAll(matcher.regex)) {
|
|
146
|
+
const run = match[3];
|
|
147
|
+
if (run === void 0) return true;
|
|
148
|
+
if (match[1] !== "END" && parseAttributeRun(run) !== void 0) return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
131
151
|
}
|
|
132
152
|
/**
|
|
133
153
|
* One compiled scanner per recognized comment style, memoized.
|
|
@@ -148,15 +168,16 @@ var SectionDialect = class SectionDialect extends Schema.Class("SectionDialect")
|
|
|
148
168
|
const tail = style.suffix === void 0 ? "" : `${GAP}${escapeRegex(style.suffix)}`;
|
|
149
169
|
return {
|
|
150
170
|
style,
|
|
151
|
-
regex: new RegExp(`^${escapeRegex(style.prefix)}${GAP}---${GAP}(BEGIN|END)${GAP}${KEY_CAPTURE}${GAP}${phrase}
|
|
171
|
+
regex: new RegExp(`^${escapeRegex(style.prefix)}${GAP}---${GAP}(BEGIN|END)${GAP}${KEY_CAPTURE}${GAP}${phrase}(?:${GAP}([^ \\t\\r\\n][^\\r\\n]*?))?${GAP}---${tail}[ \\t]*(?=\r?$)`, "gm")
|
|
152
172
|
};
|
|
153
173
|
});
|
|
154
174
|
matcherCache.set(this, compiled);
|
|
155
175
|
return compiled;
|
|
156
176
|
}
|
|
157
|
-
marker(kind, id) {
|
|
177
|
+
marker(kind, id, attributes) {
|
|
158
178
|
const tail = id.commentStyle.suffix === void 0 ? "" : ` ${id.commentStyle.suffix}`;
|
|
159
|
-
|
|
179
|
+
const run = attributes === void 0 ? "" : Object.entries(attributes).map(([name, value]) => ` ${name}="${value}"`).join("");
|
|
180
|
+
return `${id.commentStyle.prefix} --- ${kind} ${id.key} ${this.phrase}${run} ---${tail}`;
|
|
160
181
|
}
|
|
161
182
|
};
|
|
162
183
|
|
package/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ declare const CommentStyle_base: Schema.Class<CommentStyle, Schema.Struct<{
|
|
|
29
29
|
*
|
|
30
30
|
* @public
|
|
31
31
|
*/
|
|
32
|
-
declare class CommentStyle extends CommentStyle_base {
|
|
32
|
+
export declare class CommentStyle extends CommentStyle_base {
|
|
33
33
|
/** Shell, YAML, TOML, Python, Dockerfile, `.env`. */
|
|
34
34
|
static readonly hash: CommentStyle;
|
|
35
35
|
/** JavaScript, TypeScript, C, Go, Rust, JSONC. */
|
|
@@ -88,7 +88,7 @@ declare class CommentStyle extends CommentStyle_base {
|
|
|
88
88
|
*
|
|
89
89
|
* @public
|
|
90
90
|
*/
|
|
91
|
-
declare const SectionKey: Schema.String;
|
|
91
|
+
export declare const SectionKey: Schema.String;
|
|
92
92
|
declare const SectionId_base: Schema.Class<SectionId, Schema.Struct<{
|
|
93
93
|
/** The section's name, exactly as it appears in the markers. */
|
|
94
94
|
readonly key: Schema.String;
|
|
@@ -118,9 +118,16 @@ declare const SectionId_base: Schema.Class<SectionId, Schema.Struct<{
|
|
|
118
118
|
*
|
|
119
119
|
* @public
|
|
120
120
|
*/
|
|
121
|
-
declare class SectionId extends SectionId_base {
|
|
122
|
-
/**
|
|
123
|
-
|
|
121
|
+
export declare class SectionId extends SectionId_base {
|
|
122
|
+
/**
|
|
123
|
+
* Pair this identity with the content a tool wants inside it.
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* `attributes` become `name="value"` pairs on the section's BEGIN marker,
|
|
127
|
+
* emitted in the record's insertion order. They are metadata, not identity:
|
|
128
|
+
* see {@link Section} for the grammar and the equality rules.
|
|
129
|
+
*/
|
|
130
|
+
section(content: string, attributes?: Readonly<Record<string, string>>): Section;
|
|
124
131
|
}
|
|
125
132
|
declare const Section_base: Schema.Class<Section, Schema.Struct<{
|
|
126
133
|
/** The section's name, exactly as it appears in the markers. */
|
|
@@ -129,6 +136,8 @@ declare const Section_base: Schema.Class<Section, Schema.Struct<{
|
|
|
129
136
|
readonly commentStyle: typeof CommentStyle;
|
|
130
137
|
/** Everything between the markers, exclusive of the boundary line breaks. */
|
|
131
138
|
readonly content: Schema.String;
|
|
139
|
+
/** The BEGIN marker's `name="value"` pairs. Empty when the marker carries none. */
|
|
140
|
+
readonly attributes: Schema.withConstructorDefault<Schema.$Record<Schema.String, Schema.String>>;
|
|
132
141
|
}>, {}>;
|
|
133
142
|
/**
|
|
134
143
|
* A managed section: an identity plus the content its owner wants between the
|
|
@@ -143,12 +152,27 @@ declare const Section_base: Schema.Class<Section, Schema.Struct<{
|
|
|
143
152
|
* parse time rather than inside equality, so `Equal.equals` stays honest for
|
|
144
153
|
* a consumer comparing two sections directly.
|
|
145
154
|
*
|
|
155
|
+
* `attributes` are `name="value"` pairs carried on the BEGIN marker — metadata
|
|
156
|
+
* a tool wants readable from the marker line itself, without opening the
|
|
157
|
+
* content. They **participate in equality** (an attribute change is real
|
|
158
|
+
* drift) but **never in identity**: the section a marker names is decided by
|
|
159
|
+
* key and comment style alone, so changing an attribute updates a block in
|
|
160
|
+
* place rather than orphaning it. Equality over attributes is record equality
|
|
161
|
+
* — order-insensitive, so two sections carrying the same pairs in different
|
|
162
|
+
* insertion orders compare equal — while an actual rewrite renders the pairs
|
|
163
|
+
* in the record's insertion order. Omitting the field and passing `{}` are
|
|
164
|
+
* the same section: the constructor defaults to an empty record.
|
|
165
|
+
*
|
|
166
|
+
* Attribute names must match `[A-Za-z][A-Za-z0-9_-]*` and values may not
|
|
167
|
+
* contain `"` or a line break; violations fail typed at render, not here, so
|
|
168
|
+
* runtime data can never turn construction into a defect.
|
|
169
|
+
*
|
|
146
170
|
* @public
|
|
147
171
|
*/
|
|
148
|
-
declare class Section extends Section_base {
|
|
172
|
+
export declare class Section extends Section_base {
|
|
149
173
|
/** This section's identity, without its content. */
|
|
150
174
|
get id(): SectionId;
|
|
151
|
-
/** The same section carrying different content. */
|
|
175
|
+
/** The same section carrying different content. Attributes are preserved. */
|
|
152
176
|
withContent(content: string): Section;
|
|
153
177
|
}
|
|
154
178
|
declare const PlacedSection_base: Schema.Class<PlacedSection, Schema.Struct<{
|
|
@@ -172,7 +196,7 @@ declare const PlacedSection_base: Schema.Class<PlacedSection, Schema.Struct<{
|
|
|
172
196
|
*
|
|
173
197
|
* @public
|
|
174
198
|
*/
|
|
175
|
-
declare class PlacedSection extends PlacedSection_base {}
|
|
199
|
+
export declare class PlacedSection extends PlacedSection_base {}
|
|
176
200
|
//#endregion
|
|
177
201
|
//#region src/SectionDialect.d.ts
|
|
178
202
|
/**
|
|
@@ -192,10 +216,16 @@ declare const SectionRenderError_base: Schema.Class<SectionRenderError, Schema.T
|
|
|
192
216
|
* declared twice in one call, so the caller stated two intentions for one
|
|
193
217
|
* block and any choice between them would be a guess; this is the
|
|
194
218
|
* caller-side twin of the document-side `duplicateSection`.
|
|
219
|
+
* `invalidAttribute` — an attribute's name is outside the
|
|
220
|
+
* `[A-Za-z][A-Za-z0-9_-]*` grammar, or its value contains `"` or a line
|
|
221
|
+
* break; either would render a marker the scanner could not read back
|
|
222
|
+
* verbatim, and there is no escaping mechanism by design.
|
|
195
223
|
*/
|
|
196
|
-
readonly reason: Schema.Literals<readonly ["markerInContent", "unknownCommentStyle", "duplicateDeclaration"]>;
|
|
224
|
+
readonly reason: Schema.Literals<readonly ["markerInContent", "unknownCommentStyle", "duplicateDeclaration", "invalidAttribute"]>;
|
|
197
225
|
/** The key of the section that could not be rendered. */
|
|
198
226
|
readonly key: Schema.String;
|
|
227
|
+
/** The offending attribute's name, when the refusal names one. */
|
|
228
|
+
readonly attribute: Schema.optionalKey<Schema.String>;
|
|
199
229
|
}>, import("effect/Cause").YieldableError>;
|
|
200
230
|
/**
|
|
201
231
|
* Raised when a section cannot be turned into marker-delimited text.
|
|
@@ -207,7 +237,7 @@ declare const SectionRenderError_base: Schema.Class<SectionRenderError, Schema.T
|
|
|
207
237
|
*
|
|
208
238
|
* @public
|
|
209
239
|
*/
|
|
210
|
-
declare class SectionRenderError extends SectionRenderError_base {
|
|
240
|
+
export declare class SectionRenderError extends SectionRenderError_base {
|
|
211
241
|
get message(): string;
|
|
212
242
|
}
|
|
213
243
|
declare const SectionDialect_base: Schema.Class<SectionDialect, Schema.Struct<{
|
|
@@ -239,7 +269,7 @@ declare const SectionDialect_base: Schema.Class<SectionDialect, Schema.Struct<{
|
|
|
239
269
|
*
|
|
240
270
|
* @public
|
|
241
271
|
*/
|
|
242
|
-
declare class SectionDialect extends SectionDialect_base {
|
|
272
|
+
export declare class SectionDialect extends SectionDialect_base {
|
|
243
273
|
/**
|
|
244
274
|
* The zero-configuration dialect: the phrase `MANAGED SECTION` and every
|
|
245
275
|
* preset comment style.
|
|
@@ -299,7 +329,7 @@ declare class SectionDialect extends SectionDialect_base {
|
|
|
299
329
|
*
|
|
300
330
|
* @public
|
|
301
331
|
*/
|
|
302
|
-
type SyncOutcome = Data.TaggedEnum<{
|
|
332
|
+
export type SyncOutcome = Data.TaggedEnum<{
|
|
303
333
|
/** The section was not in the document and has been added. */
|
|
304
334
|
readonly Created: {
|
|
305
335
|
readonly section: Section;
|
|
@@ -320,7 +350,7 @@ type SyncOutcome = Data.TaggedEnum<{
|
|
|
320
350
|
*
|
|
321
351
|
* @public
|
|
322
352
|
*/
|
|
323
|
-
declare const SyncOutcome: {
|
|
353
|
+
export declare const SyncOutcome: {
|
|
324
354
|
readonly $is: <Tag extends "Created" | "Unchanged" | "Updated">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
325
355
|
readonly _tag: "Created";
|
|
326
356
|
readonly section: Section;
|
|
@@ -415,7 +445,7 @@ declare const SyncOutcome: {
|
|
|
415
445
|
*
|
|
416
446
|
* @public
|
|
417
447
|
*/
|
|
418
|
-
type CheckOutcome = Data.TaggedEnum<{
|
|
448
|
+
export type CheckOutcome = Data.TaggedEnum<{
|
|
419
449
|
/** No section with this identity is in the document. */
|
|
420
450
|
readonly Absent: {
|
|
421
451
|
readonly id: SectionId;
|
|
@@ -436,7 +466,7 @@ type CheckOutcome = Data.TaggedEnum<{
|
|
|
436
466
|
*
|
|
437
467
|
* @public
|
|
438
468
|
*/
|
|
439
|
-
declare const CheckOutcome: {
|
|
469
|
+
export declare const CheckOutcome: {
|
|
440
470
|
readonly $is: <Tag extends "Absent" | "Drifted" | "UpToDate">(tag: Tag) => (u: unknown) => u is Extract<{
|
|
441
471
|
readonly _tag: "Absent";
|
|
442
472
|
readonly id: SectionId;
|
|
@@ -546,7 +576,7 @@ declare const SectionParseError_base: Schema.Class<SectionParseError, Schema.Tag
|
|
|
546
576
|
*
|
|
547
577
|
* @public
|
|
548
578
|
*/
|
|
549
|
-
declare class SectionParseError extends SectionParseError_base {
|
|
579
|
+
export declare class SectionParseError extends SectionParseError_base {
|
|
550
580
|
get message(): string;
|
|
551
581
|
/**
|
|
552
582
|
* The same failure, attributed to a file.
|
|
@@ -610,7 +640,7 @@ declare const SectionDocument_base: Schema.Class<SectionDocument, Schema.Struct<
|
|
|
610
640
|
*
|
|
611
641
|
* @public
|
|
612
642
|
*/
|
|
613
|
-
declare class SectionDocument extends SectionDocument_base {
|
|
643
|
+
export declare class SectionDocument extends SectionDocument_base {
|
|
614
644
|
/**
|
|
615
645
|
* Parse a document. The synchronous primitive.
|
|
616
646
|
*
|
|
@@ -688,7 +718,7 @@ declare const SectionFileError_base: Schema.Class<SectionFileError, Schema.Tagge
|
|
|
688
718
|
*
|
|
689
719
|
* @public
|
|
690
720
|
*/
|
|
691
|
-
declare class SectionFileError extends SectionFileError_base {
|
|
721
|
+
export declare class SectionFileError extends SectionFileError_base {
|
|
692
722
|
get message(): string;
|
|
693
723
|
}
|
|
694
724
|
/**
|
|
@@ -774,7 +804,7 @@ declare const ManagedSection_base: Context.ServiceClass<ManagedSection, "@effect
|
|
|
774
804
|
*
|
|
775
805
|
* @public
|
|
776
806
|
*/
|
|
777
|
-
declare class ManagedSection extends ManagedSection_base {
|
|
807
|
+
export declare class ManagedSection extends ManagedSection_base {
|
|
778
808
|
/**
|
|
779
809
|
* The default layer, reading and writing {@link SectionDialect.default}
|
|
780
810
|
* markers.
|
|
@@ -808,5 +838,5 @@ declare class ManagedSection extends ManagedSection_base {
|
|
|
808
838
|
static readonly layerTest: (overrides?: Partial<ManagedSectionShape>) => Layer.Layer<ManagedSection>;
|
|
809
839
|
}
|
|
810
840
|
//#endregion
|
|
811
|
-
export {
|
|
841
|
+
export type { Eol, ManagedSectionOptions, ManagedSectionShape, SectionReconciliation };
|
|
812
842
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/internal/attributes.ts
|
|
2
|
+
/** The attribute name grammar. No leading digit, underscore or dash. */
|
|
3
|
+
const ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
4
|
+
/** True when a value can appear inside an attribute's double quotes verbatim. */
|
|
5
|
+
const isValidAttributeValue = (value) => !value.includes("\"") && !value.includes("\n") && !value.includes("\r");
|
|
6
|
+
const isNameStart = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
7
|
+
const isNameChar = (code) => isNameStart(code) || code >= 48 && code <= 57 || code === 95 || code === 45;
|
|
8
|
+
const isSeparator = (code) => code === 32 || code === 9;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a captured attribute run, or refuse it.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* `undefined` means the run is not a valid attribute run and the line carrying
|
|
14
|
+
* it is **not a marker** — it is ordinary content. That covers a mangled pair
|
|
15
|
+
* and a name declared twice: two values for one name is two intentions for one
|
|
16
|
+
* attribute, and any choice between them would be a guess.
|
|
17
|
+
*
|
|
18
|
+
* Document order is preserved: the returned record's insertion order is the
|
|
19
|
+
* order the pairs appear in the marker.
|
|
20
|
+
*
|
|
21
|
+
* A hand-rolled single pass rather than a validating regex: the run comes off
|
|
22
|
+
* an untrusted document line, and the natural `(pair)(sep pair)*$` pattern
|
|
23
|
+
* backtracks polynomially on hostile near-misses. This walk touches each
|
|
24
|
+
* character exactly once, so a megabyte of adversarial line costs a megabyte
|
|
25
|
+
* of work.
|
|
26
|
+
*/
|
|
27
|
+
const parseAttributeRun = (run) => {
|
|
28
|
+
const attributes = {};
|
|
29
|
+
const length = run.length;
|
|
30
|
+
let index = 0;
|
|
31
|
+
let first = true;
|
|
32
|
+
while (index < length) {
|
|
33
|
+
if (!first) {
|
|
34
|
+
if (!isSeparator(run.charCodeAt(index))) return;
|
|
35
|
+
while (index < length && isSeparator(run.charCodeAt(index))) index += 1;
|
|
36
|
+
if (index >= length) return;
|
|
37
|
+
}
|
|
38
|
+
first = false;
|
|
39
|
+
if (!isNameStart(run.charCodeAt(index))) return;
|
|
40
|
+
const nameStart = index;
|
|
41
|
+
index += 1;
|
|
42
|
+
while (index < length && isNameChar(run.charCodeAt(index))) index += 1;
|
|
43
|
+
const name = run.slice(nameStart, index);
|
|
44
|
+
if (index + 1 >= length || run.charCodeAt(index) !== 61 || run.charCodeAt(index + 1) !== 34) return;
|
|
45
|
+
index += 2;
|
|
46
|
+
const valueStart = index;
|
|
47
|
+
while (index < length) {
|
|
48
|
+
const code = run.charCodeAt(index);
|
|
49
|
+
if (code === 34) break;
|
|
50
|
+
if (code === 10 || code === 13) return;
|
|
51
|
+
index += 1;
|
|
52
|
+
}
|
|
53
|
+
if (index >= length) return;
|
|
54
|
+
const value = run.slice(valueStart, index);
|
|
55
|
+
index += 1;
|
|
56
|
+
if (Object.hasOwn(attributes, name)) return;
|
|
57
|
+
attributes[name] = value;
|
|
58
|
+
}
|
|
59
|
+
return first ? void 0 : attributes;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { ATTRIBUTE_NAME_PATTERN, isValidAttributeValue, parseAttributeRun };
|
package/internal/scan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseAttributeRun } from "./attributes.js";
|
|
1
2
|
import { PlacedSection, Section } from "../Section.js";
|
|
2
3
|
|
|
3
4
|
//#region src/internal/scan.ts
|
|
@@ -49,13 +50,21 @@ const collectHits = (text, dialect) => {
|
|
|
49
50
|
for (const matcher of dialect.matchers()) for (const match of text.matchAll(matcher.regex)) {
|
|
50
51
|
const start = match.index;
|
|
51
52
|
if (start === void 0 || seen.has(start)) continue;
|
|
53
|
+
const run = match[3];
|
|
54
|
+
let attributes;
|
|
55
|
+
if (run !== void 0) {
|
|
56
|
+
if (match[1] === "END") continue;
|
|
57
|
+
attributes = parseAttributeRun(run);
|
|
58
|
+
if (attributes === void 0) continue;
|
|
59
|
+
}
|
|
52
60
|
seen.add(start);
|
|
53
61
|
hits.push({
|
|
54
62
|
kind: match[1] === "BEGIN" ? "BEGIN" : "END",
|
|
55
63
|
key: match[2] ?? "",
|
|
56
64
|
style: matcher.style,
|
|
57
65
|
start,
|
|
58
|
-
end: start + match[0].length
|
|
66
|
+
end: start + match[0].length,
|
|
67
|
+
...attributes === void 0 ? {} : { attributes }
|
|
59
68
|
});
|
|
60
69
|
}
|
|
61
70
|
return hits.sort((left, right) => left.start - right.start);
|
|
@@ -109,7 +118,8 @@ const scan = (text, dialect) => {
|
|
|
109
118
|
section: Section.make({
|
|
110
119
|
key: open.key,
|
|
111
120
|
commentStyle: open.style,
|
|
112
|
-
content: normalizeEol(inner)
|
|
121
|
+
content: normalizeEol(inner),
|
|
122
|
+
...open.attributes === void 0 ? {} : { attributes: open.attributes }
|
|
113
123
|
}),
|
|
114
124
|
start: open.start,
|
|
115
125
|
end: hit.end,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Managed-section blocks in user-editable files: parse, reconcile, sync and check delimited regions.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"effect": "4.0.0-rc.
|
|
40
|
+
"effect": "4.0.0-rc.112"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=24.11.0"
|