@proseql/core 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/LICENSE +21 -0
- package/dist/errors/crud-errors.d.ts +98 -0
- package/dist/errors/crud-errors.d.ts.map +1 -0
- package/dist/errors/crud-errors.js +23 -0
- package/dist/errors/crud-errors.js.map +1 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +12 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/migration-errors.d.ts +22 -0
- package/dist/errors/migration-errors.d.ts.map +1 -0
- package/dist/errors/migration-errors.js +14 -0
- package/dist/errors/migration-errors.js.map +1 -0
- package/dist/errors/plugin-errors.d.ts +15 -0
- package/dist/errors/plugin-errors.d.ts.map +1 -0
- package/dist/errors/plugin-errors.js +11 -0
- package/dist/errors/plugin-errors.js.map +1 -0
- package/dist/errors/query-errors.d.ts +31 -0
- package/dist/errors/query-errors.d.ts.map +1 -0
- package/dist/errors/query-errors.js +11 -0
- package/dist/errors/query-errors.js.map +1 -0
- package/dist/errors/storage-errors.d.ts +30 -0
- package/dist/errors/storage-errors.d.ts.map +1 -0
- package/dist/errors/storage-errors.js +11 -0
- package/dist/errors/storage-errors.js.map +1 -0
- package/dist/factories/crud-factory-with-relationships.d.ts +28 -0
- package/dist/factories/crud-factory-with-relationships.d.ts.map +1 -0
- package/dist/factories/crud-factory-with-relationships.js +8 -0
- package/dist/factories/crud-factory-with-relationships.js.map +1 -0
- package/dist/factories/crud-factory.d.ts +25 -0
- package/dist/factories/crud-factory.d.ts.map +1 -0
- package/dist/factories/crud-factory.js +8 -0
- package/dist/factories/crud-factory.js.map +1 -0
- package/dist/factories/database-effect.d.ts +241 -0
- package/dist/factories/database-effect.d.ts.map +1 -0
- package/dist/factories/database-effect.js +859 -0
- package/dist/factories/database-effect.js.map +1 -0
- package/dist/hooks/hook-runner.d.ts +60 -0
- package/dist/hooks/hook-runner.d.ts.map +1 -0
- package/dist/hooks/hook-runner.js +107 -0
- package/dist/hooks/hook-runner.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/indexes/index-lookup.d.ts +33 -0
- package/dist/indexes/index-lookup.d.ts.map +1 -0
- package/dist/indexes/index-lookup.js +180 -0
- package/dist/indexes/index-lookup.js.map +1 -0
- package/dist/indexes/index-manager.d.ts +118 -0
- package/dist/indexes/index-manager.d.ts.map +1 -0
- package/dist/indexes/index-manager.js +345 -0
- package/dist/indexes/index-manager.js.map +1 -0
- package/dist/indexes/search-index.d.ts +179 -0
- package/dist/indexes/search-index.d.ts.map +1 -0
- package/dist/indexes/search-index.js +405 -0
- package/dist/indexes/search-index.js.map +1 -0
- package/dist/migrations/migration-runner.d.ts +70 -0
- package/dist/migrations/migration-runner.d.ts.map +1 -0
- package/dist/migrations/migration-runner.js +271 -0
- package/dist/migrations/migration-runner.js.map +1 -0
- package/dist/migrations/migration-types.d.ts +63 -0
- package/dist/migrations/migration-types.d.ts.map +1 -0
- package/dist/migrations/migration-types.js +5 -0
- package/dist/migrations/migration-types.js.map +1 -0
- package/dist/operations/crud/create-with-relationships.d.ts +44 -0
- package/dist/operations/crud/create-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/create-with-relationships.js +483 -0
- package/dist/operations/crud/create-with-relationships.js.map +1 -0
- package/dist/operations/crud/create.d.ts +48 -0
- package/dist/operations/crud/create.d.ts.map +1 -0
- package/dist/operations/crud/create.js +333 -0
- package/dist/operations/crud/create.js.map +1 -0
- package/dist/operations/crud/delete-with-relationships.d.ts +63 -0
- package/dist/operations/crud/delete-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/delete-with-relationships.js +395 -0
- package/dist/operations/crud/delete-with-relationships.js.map +1 -0
- package/dist/operations/crud/delete.d.ts +58 -0
- package/dist/operations/crud/delete.d.ts.map +1 -0
- package/dist/operations/crud/delete.js +267 -0
- package/dist/operations/crud/delete.js.map +1 -0
- package/dist/operations/crud/unique-check.d.ts +114 -0
- package/dist/operations/crud/unique-check.d.ts.map +1 -0
- package/dist/operations/crud/unique-check.js +383 -0
- package/dist/operations/crud/unique-check.js.map +1 -0
- package/dist/operations/crud/update-with-relationships.d.ts +45 -0
- package/dist/operations/crud/update-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/update-with-relationships.js +516 -0
- package/dist/operations/crud/update-with-relationships.js.map +1 -0
- package/dist/operations/crud/update.d.ts +91 -0
- package/dist/operations/crud/update.d.ts.map +1 -0
- package/dist/operations/crud/update.js +505 -0
- package/dist/operations/crud/update.js.map +1 -0
- package/dist/operations/crud/upsert.d.ts +52 -0
- package/dist/operations/crud/upsert.d.ts.map +1 -0
- package/dist/operations/crud/upsert.js +386 -0
- package/dist/operations/crud/upsert.js.map +1 -0
- package/dist/operations/query/aggregate.d.ts +30 -0
- package/dist/operations/query/aggregate.d.ts.map +1 -0
- package/dist/operations/query/aggregate.js +227 -0
- package/dist/operations/query/aggregate.js.map +1 -0
- package/dist/operations/query/cursor-stream.d.ts +18 -0
- package/dist/operations/query/cursor-stream.d.ts.map +1 -0
- package/dist/operations/query/cursor-stream.js +199 -0
- package/dist/operations/query/cursor-stream.js.map +1 -0
- package/dist/operations/query/filter-stream.d.ts +12 -0
- package/dist/operations/query/filter-stream.d.ts.map +1 -0
- package/dist/operations/query/filter-stream.js +167 -0
- package/dist/operations/query/filter-stream.js.map +1 -0
- package/dist/operations/query/filter.d.ts +13 -0
- package/dist/operations/query/filter.d.ts.map +1 -0
- package/dist/operations/query/filter.js +267 -0
- package/dist/operations/query/filter.js.map +1 -0
- package/dist/operations/query/paginate-stream.d.ts +11 -0
- package/dist/operations/query/paginate-stream.d.ts.map +1 -0
- package/dist/operations/query/paginate-stream.js +22 -0
- package/dist/operations/query/paginate-stream.js.map +1 -0
- package/dist/operations/query/query-helpers.d.ts +14 -0
- package/dist/operations/query/query-helpers.d.ts.map +1 -0
- package/dist/operations/query/query-helpers.js +22 -0
- package/dist/operations/query/query-helpers.js.map +1 -0
- package/dist/operations/query/resolve-computed.d.ts +142 -0
- package/dist/operations/query/resolve-computed.d.ts.map +1 -0
- package/dist/operations/query/resolve-computed.js +197 -0
- package/dist/operations/query/resolve-computed.js.map +1 -0
- package/dist/operations/query/search.d.ts +110 -0
- package/dist/operations/query/search.d.ts.map +1 -0
- package/dist/operations/query/search.js +188 -0
- package/dist/operations/query/search.js.map +1 -0
- package/dist/operations/query/select-stream.d.ts +27 -0
- package/dist/operations/query/select-stream.d.ts.map +1 -0
- package/dist/operations/query/select-stream.js +88 -0
- package/dist/operations/query/select-stream.js.map +1 -0
- package/dist/operations/query/select.d.ts +54 -0
- package/dist/operations/query/select.d.ts.map +1 -0
- package/dist/operations/query/select.js +159 -0
- package/dist/operations/query/select.js.map +1 -0
- package/dist/operations/query/sort-stream.d.ts +46 -0
- package/dist/operations/query/sort-stream.d.ts.map +1 -0
- package/dist/operations/query/sort-stream.js +158 -0
- package/dist/operations/query/sort-stream.js.map +1 -0
- package/dist/operations/query/sort.d.ts +9 -0
- package/dist/operations/query/sort.d.ts.map +1 -0
- package/dist/operations/query/sort.js +58 -0
- package/dist/operations/query/sort.js.map +1 -0
- package/dist/operations/relationships/populate-stream.d.ts +29 -0
- package/dist/operations/relationships/populate-stream.d.ts.map +1 -0
- package/dist/operations/relationships/populate-stream.js +159 -0
- package/dist/operations/relationships/populate-stream.js.map +1 -0
- package/dist/operations/relationships/populate.d.ts +15 -0
- package/dist/operations/relationships/populate.d.ts.map +1 -0
- package/dist/operations/relationships/populate.js +228 -0
- package/dist/operations/relationships/populate.js.map +1 -0
- package/dist/plugins/plugin-hooks.d.ts +25 -0
- package/dist/plugins/plugin-hooks.d.ts.map +1 -0
- package/dist/plugins/plugin-hooks.js +64 -0
- package/dist/plugins/plugin-hooks.js.map +1 -0
- package/dist/plugins/plugin-registry.d.ts +26 -0
- package/dist/plugins/plugin-registry.d.ts.map +1 -0
- package/dist/plugins/plugin-registry.js +150 -0
- package/dist/plugins/plugin-registry.js.map +1 -0
- package/dist/plugins/plugin-types.d.ts +95 -0
- package/dist/plugins/plugin-types.d.ts.map +1 -0
- package/dist/plugins/plugin-types.js +6 -0
- package/dist/plugins/plugin-types.js.map +1 -0
- package/dist/plugins/plugin-validation.d.ts +49 -0
- package/dist/plugins/plugin-validation.d.ts.map +1 -0
- package/dist/plugins/plugin-validation.js +295 -0
- package/dist/plugins/plugin-validation.js.map +1 -0
- package/dist/reactive/change-event.d.ts +44 -0
- package/dist/reactive/change-event.d.ts.map +1 -0
- package/dist/reactive/change-event.js +49 -0
- package/dist/reactive/change-event.js.map +1 -0
- package/dist/reactive/change-pubsub.d.ts +32 -0
- package/dist/reactive/change-pubsub.d.ts.map +1 -0
- package/dist/reactive/change-pubsub.js +31 -0
- package/dist/reactive/change-pubsub.js.map +1 -0
- package/dist/reactive/evaluate-query.d.ts +62 -0
- package/dist/reactive/evaluate-query.d.ts.map +1 -0
- package/dist/reactive/evaluate-query.js +57 -0
- package/dist/reactive/evaluate-query.js.map +1 -0
- package/dist/reactive/watch-by-id.d.ts +53 -0
- package/dist/reactive/watch-by-id.d.ts.map +1 -0
- package/dist/reactive/watch-by-id.js +55 -0
- package/dist/reactive/watch-by-id.js.map +1 -0
- package/dist/reactive/watch.d.ts +78 -0
- package/dist/reactive/watch.d.ts.map +1 -0
- package/dist/reactive/watch.js +133 -0
- package/dist/reactive/watch.js.map +1 -0
- package/dist/serializers/codecs/hjson.d.ts +33 -0
- package/dist/serializers/codecs/hjson.d.ts.map +1 -0
- package/dist/serializers/codecs/hjson.js +40 -0
- package/dist/serializers/codecs/hjson.js.map +1 -0
- package/dist/serializers/codecs/json.d.ts +22 -0
- package/dist/serializers/codecs/json.d.ts.map +1 -0
- package/dist/serializers/codecs/json.js +28 -0
- package/dist/serializers/codecs/json.js.map +1 -0
- package/dist/serializers/codecs/json5.d.ts +26 -0
- package/dist/serializers/codecs/json5.d.ts.map +1 -0
- package/dist/serializers/codecs/json5.js +33 -0
- package/dist/serializers/codecs/json5.js.map +1 -0
- package/dist/serializers/codecs/jsonc.d.ts +29 -0
- package/dist/serializers/codecs/jsonc.d.ts.map +1 -0
- package/dist/serializers/codecs/jsonc.js +38 -0
- package/dist/serializers/codecs/jsonc.js.map +1 -0
- package/dist/serializers/codecs/jsonl.d.ts +17 -0
- package/dist/serializers/codecs/jsonl.d.ts.map +1 -0
- package/dist/serializers/codecs/jsonl.js +31 -0
- package/dist/serializers/codecs/jsonl.js.map +1 -0
- package/dist/serializers/codecs/prose.d.ts +419 -0
- package/dist/serializers/codecs/prose.d.ts.map +1 -0
- package/dist/serializers/codecs/prose.js +1060 -0
- package/dist/serializers/codecs/prose.js.map +1 -0
- package/dist/serializers/codecs/toml.d.ts +23 -0
- package/dist/serializers/codecs/toml.d.ts.map +1 -0
- package/dist/serializers/codecs/toml.js +66 -0
- package/dist/serializers/codecs/toml.js.map +1 -0
- package/dist/serializers/codecs/toon.d.ts +20 -0
- package/dist/serializers/codecs/toon.d.ts.map +1 -0
- package/dist/serializers/codecs/toon.js +33 -0
- package/dist/serializers/codecs/toon.js.map +1 -0
- package/dist/serializers/codecs/yaml.d.ts +24 -0
- package/dist/serializers/codecs/yaml.d.ts.map +1 -0
- package/dist/serializers/codecs/yaml.js +31 -0
- package/dist/serializers/codecs/yaml.js.map +1 -0
- package/dist/serializers/format-codec.d.ts +53 -0
- package/dist/serializers/format-codec.d.ts.map +1 -0
- package/dist/serializers/format-codec.js +148 -0
- package/dist/serializers/format-codec.js.map +1 -0
- package/dist/serializers/presets.d.ts +48 -0
- package/dist/serializers/presets.d.ts.map +1 -0
- package/dist/serializers/presets.js +72 -0
- package/dist/serializers/presets.js.map +1 -0
- package/dist/serializers/serializer-service.d.ts +11 -0
- package/dist/serializers/serializer-service.d.ts.map +1 -0
- package/dist/serializers/serializer-service.js +4 -0
- package/dist/serializers/serializer-service.js.map +1 -0
- package/dist/state/collection-state.d.ts +19 -0
- package/dist/state/collection-state.d.ts.map +1 -0
- package/dist/state/collection-state.js +15 -0
- package/dist/state/collection-state.js.map +1 -0
- package/dist/state/state-operations.d.ts +38 -0
- package/dist/state/state-operations.d.ts.map +1 -0
- package/dist/state/state-operations.js +65 -0
- package/dist/state/state-operations.js.map +1 -0
- package/dist/storage/in-memory-adapter-layer.d.ts +16 -0
- package/dist/storage/in-memory-adapter-layer.d.ts.map +1 -0
- package/dist/storage/in-memory-adapter-layer.js +81 -0
- package/dist/storage/in-memory-adapter-layer.js.map +1 -0
- package/dist/storage/persistence-effect.d.ts +244 -0
- package/dist/storage/persistence-effect.d.ts.map +1 -0
- package/dist/storage/persistence-effect.js +551 -0
- package/dist/storage/persistence-effect.js.map +1 -0
- package/dist/storage/storage-service.d.ts +22 -0
- package/dist/storage/storage-service.d.ts.map +1 -0
- package/dist/storage/storage-service.js +4 -0
- package/dist/storage/storage-service.js.map +1 -0
- package/dist/storage/transforms.d.ts +183 -0
- package/dist/storage/transforms.d.ts.map +1 -0
- package/dist/storage/transforms.js +263 -0
- package/dist/storage/transforms.js.map +1 -0
- package/dist/transactions/transaction.d.ts +87 -0
- package/dist/transactions/transaction.d.ts.map +1 -0
- package/dist/transactions/transaction.js +240 -0
- package/dist/transactions/transaction.js.map +1 -0
- package/dist/types/aggregate-types.d.ts +73 -0
- package/dist/types/aggregate-types.d.ts.map +1 -0
- package/dist/types/aggregate-types.js +14 -0
- package/dist/types/aggregate-types.js.map +1 -0
- package/dist/types/computed-types.d.ts +71 -0
- package/dist/types/computed-types.d.ts.map +1 -0
- package/dist/types/computed-types.js +8 -0
- package/dist/types/computed-types.js.map +1 -0
- package/dist/types/crud-relationship-types.d.ts +180 -0
- package/dist/types/crud-relationship-types.d.ts.map +1 -0
- package/dist/types/crud-relationship-types.js +17 -0
- package/dist/types/crud-relationship-types.js.map +1 -0
- package/dist/types/crud-types.d.ts +343 -0
- package/dist/types/crud-types.d.ts.map +1 -0
- package/dist/types/crud-types.js +43 -0
- package/dist/types/crud-types.js.map +1 -0
- package/dist/types/cursor-types.d.ts +52 -0
- package/dist/types/cursor-types.d.ts.map +1 -0
- package/dist/types/cursor-types.js +2 -0
- package/dist/types/cursor-types.js.map +1 -0
- package/dist/types/database-config-types.d.ts +196 -0
- package/dist/types/database-config-types.d.ts.map +1 -0
- package/dist/types/database-config-types.js +11 -0
- package/dist/types/database-config-types.js.map +1 -0
- package/dist/types/hook-types.d.ts +158 -0
- package/dist/types/hook-types.d.ts.map +1 -0
- package/dist/types/hook-types.js +6 -0
- package/dist/types/hook-types.js.map +1 -0
- package/dist/types/index-types.d.ts +42 -0
- package/dist/types/index-types.d.ts.map +1 -0
- package/dist/types/index-types.js +8 -0
- package/dist/types/index-types.js.map +1 -0
- package/dist/types/operators.d.ts +5 -0
- package/dist/types/operators.d.ts.map +1 -0
- package/dist/types/operators.js +297 -0
- package/dist/types/operators.js.map +1 -0
- package/dist/types/query-overloads.d.ts +54 -0
- package/dist/types/query-overloads.d.ts.map +1 -0
- package/dist/types/query-overloads.js +3 -0
- package/dist/types/query-overloads.js.map +1 -0
- package/dist/types/reactive-types.d.ts +75 -0
- package/dist/types/reactive-types.d.ts.map +1 -0
- package/dist/types/reactive-types.js +7 -0
- package/dist/types/reactive-types.js.map +1 -0
- package/dist/types/schema-types.d.ts +56 -0
- package/dist/types/schema-types.d.ts.map +1 -0
- package/dist/types/schema-types.js +8 -0
- package/dist/types/schema-types.js.map +1 -0
- package/dist/types/search-types.d.ts +82 -0
- package/dist/types/search-types.d.ts.map +1 -0
- package/dist/types/search-types.js +110 -0
- package/dist/types/search-types.js.map +1 -0
- package/dist/types/types.d.ts +286 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +2 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/id-generator.d.ts +97 -0
- package/dist/utils/id-generator.d.ts.map +1 -0
- package/dist/utils/id-generator.js +247 -0
- package/dist/utils/id-generator.js.map +1 -0
- package/dist/utils/nested-path.d.ts +56 -0
- package/dist/utils/nested-path.d.ts.map +1 -0
- package/dist/utils/nested-path.js +119 -0
- package/dist/utils/nested-path.js.map +1 -0
- package/dist/utils/path.d.ts +16 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +24 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/validators/foreign-key.d.ts +49 -0
- package/dist/validators/foreign-key.d.ts.map +1 -0
- package/dist/validators/foreign-key.js +153 -0
- package/dist/validators/foreign-key.js.map +1 -0
- package/dist/validators/schema-validator.d.ts +19 -0
- package/dist/validators/schema-validator.d.ts.map +1 -0
- package/dist/validators/schema-validator.js +34 -0
- package/dist/validators/schema-validator.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Template Compiler
|
|
3
|
+
// ============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Compiles a template string into an ordered list of segments.
|
|
6
|
+
* Parses `{fieldName}` placeholders and literal text into segments.
|
|
7
|
+
*
|
|
8
|
+
* @param template - The template string with {fieldName} placeholders
|
|
9
|
+
* @returns A CompiledTemplate with segments and field names
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const compiled = compileTemplate('#{id} "{title}" by {author}')
|
|
14
|
+
* // compiled.segments = [
|
|
15
|
+
* // { type: "literal", text: "#" },
|
|
16
|
+
* // { type: "field", name: "id" },
|
|
17
|
+
* // { type: "literal", text: ' "' },
|
|
18
|
+
* // { type: "field", name: "title" },
|
|
19
|
+
* // { type: "literal", text: '" by ' },
|
|
20
|
+
* // { type: "field", name: "author" },
|
|
21
|
+
* // ]
|
|
22
|
+
* // compiled.fields = ["id", "title", "author"]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const compileTemplate = (template) => {
|
|
26
|
+
const segments = [];
|
|
27
|
+
const fields = [];
|
|
28
|
+
let pos = 0;
|
|
29
|
+
let literalStart = 0;
|
|
30
|
+
let lastSegmentWasField = false;
|
|
31
|
+
while (pos < template.length) {
|
|
32
|
+
const char = template[pos];
|
|
33
|
+
if (char === "{") {
|
|
34
|
+
// Emit any accumulated literal text before this field
|
|
35
|
+
if (pos > literalStart) {
|
|
36
|
+
segments.push({ type: "literal", text: template.slice(literalStart, pos) });
|
|
37
|
+
lastSegmentWasField = false;
|
|
38
|
+
}
|
|
39
|
+
// Check for adjacent fields with no literal separator
|
|
40
|
+
if (lastSegmentWasField) {
|
|
41
|
+
throw new Error(`Adjacent fields with no literal separator at position ${pos}: fields must be separated by literal text`);
|
|
42
|
+
}
|
|
43
|
+
// Find the closing brace
|
|
44
|
+
const closePos = template.indexOf("}", pos + 1);
|
|
45
|
+
if (closePos === -1) {
|
|
46
|
+
throw new Error(`Unclosed brace in template at position ${pos}: "${template.slice(pos)}"`);
|
|
47
|
+
}
|
|
48
|
+
// Extract the field name
|
|
49
|
+
const fieldName = template.slice(pos + 1, closePos);
|
|
50
|
+
if (fieldName.length === 0) {
|
|
51
|
+
throw new Error(`Empty field name in template at position ${pos}`);
|
|
52
|
+
}
|
|
53
|
+
segments.push({ type: "field", name: fieldName });
|
|
54
|
+
fields.push(fieldName);
|
|
55
|
+
lastSegmentWasField = true;
|
|
56
|
+
// Move past the closing brace
|
|
57
|
+
pos = closePos + 1;
|
|
58
|
+
literalStart = pos;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
pos++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Emit any trailing literal text
|
|
65
|
+
if (pos > literalStart) {
|
|
66
|
+
segments.push({ type: "literal", text: template.slice(literalStart, pos) });
|
|
67
|
+
}
|
|
68
|
+
return { segments, fields };
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Compiles an array of overflow template strings into CompiledTemplates.
|
|
72
|
+
* Each overflow template follows the same {fieldName} placeholder syntax as the headline template.
|
|
73
|
+
*
|
|
74
|
+
* @param overflow - Optional array of overflow template strings
|
|
75
|
+
* @returns An array of CompiledTemplate objects, or empty array if no overflow templates
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const compiled = compileOverflowTemplates(['tagged {tags}', '~ {description}'])
|
|
80
|
+
* // compiled[0].segments = [
|
|
81
|
+
* // { type: "literal", text: "tagged " },
|
|
82
|
+
* // { type: "field", name: "tags" },
|
|
83
|
+
* // ]
|
|
84
|
+
* // compiled[0].fields = ["tags"]
|
|
85
|
+
* // compiled[1].segments = [
|
|
86
|
+
* // { type: "literal", text: "~ " },
|
|
87
|
+
* // { type: "field", name: "description" },
|
|
88
|
+
* // ]
|
|
89
|
+
* // compiled[1].fields = ["description"]
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export const compileOverflowTemplates = (overflow) => {
|
|
93
|
+
if (!overflow || overflow.length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return overflow.map((template, index) => {
|
|
97
|
+
try {
|
|
98
|
+
return compileTemplate(template);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
throw new Error(`Error in overflow template at index ${index}: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Value Serialization
|
|
110
|
+
// ============================================================================
|
|
111
|
+
/**
|
|
112
|
+
* Serializes a value to its prose format string representation.
|
|
113
|
+
*
|
|
114
|
+
* Type mapping:
|
|
115
|
+
* - null/undefined → `~`
|
|
116
|
+
* - boolean → `true` / `false`
|
|
117
|
+
* - number → digit characters (e.g., `42`, `-3.14`)
|
|
118
|
+
* - array → `[a, b, c]` with element quoting for `,` and `]`
|
|
119
|
+
* - string → bare text (quoting for delimiters handled by encodeHeadline)
|
|
120
|
+
*
|
|
121
|
+
* @param value - The value to serialize
|
|
122
|
+
* @returns The serialized string representation
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* serializeValue(42) // "42"
|
|
127
|
+
* serializeValue(true) // "true"
|
|
128
|
+
* serializeValue(null) // "~"
|
|
129
|
+
* serializeValue("hello") // "hello"
|
|
130
|
+
* serializeValue(["a", "b"]) // "[a, b]"
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export const serializeValue = (value) => {
|
|
134
|
+
// null or undefined → tilde
|
|
135
|
+
if (value === null || value === undefined) {
|
|
136
|
+
return "~";
|
|
137
|
+
}
|
|
138
|
+
// boolean → true/false
|
|
139
|
+
if (typeof value === "boolean") {
|
|
140
|
+
return value ? "true" : "false";
|
|
141
|
+
}
|
|
142
|
+
// number → digit representation
|
|
143
|
+
if (typeof value === "number") {
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
// array → [element, element, ...]
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
const elements = value.map((element) => {
|
|
149
|
+
const serialized = serializeValue(element);
|
|
150
|
+
// Quote elements that contain comma, closing bracket, or double quote
|
|
151
|
+
if (serialized.includes(",") ||
|
|
152
|
+
serialized.includes("]") ||
|
|
153
|
+
serialized.includes('"')) {
|
|
154
|
+
return `"${serialized.replace(/"/g, '\\"')}"`;
|
|
155
|
+
}
|
|
156
|
+
return serialized;
|
|
157
|
+
});
|
|
158
|
+
return `[${elements.join(", ")}]`;
|
|
159
|
+
}
|
|
160
|
+
// string (or anything else) → bare text
|
|
161
|
+
return String(value);
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Deserializes a prose format string back to its typed value.
|
|
165
|
+
* Uses heuristic type detection:
|
|
166
|
+
* - Numbers: matches `/^-?\d+(\.\d+)?$/`
|
|
167
|
+
* - Booleans: exact match `true` or `false`
|
|
168
|
+
* - Null: exact match `~`
|
|
169
|
+
* - Arrays: starts with `[`, ends with `]`
|
|
170
|
+
* - Strings: default (anything not matching above)
|
|
171
|
+
*
|
|
172
|
+
* @param text - The serialized string to deserialize
|
|
173
|
+
* @returns The deserialized value with its inferred type
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* deserializeValue("42") // 42 (number)
|
|
178
|
+
* deserializeValue("-3.14") // -3.14 (number)
|
|
179
|
+
* deserializeValue("true") // true (boolean)
|
|
180
|
+
* deserializeValue("false") // false (boolean)
|
|
181
|
+
* deserializeValue("~") // null
|
|
182
|
+
* deserializeValue("[a, b, c]") // ["a", "b", "c"] (array)
|
|
183
|
+
* deserializeValue("hello") // "hello" (string)
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export const deserializeValue = (text) => {
|
|
187
|
+
// null → tilde
|
|
188
|
+
if (text === "~") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
// boolean → true/false exact match
|
|
192
|
+
if (text === "true") {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (text === "false") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// number → matches /^-?\d+(\.\d+)?$/
|
|
199
|
+
const numberRegex = /^-?\d+(\.\d+)?$/;
|
|
200
|
+
if (numberRegex.test(text)) {
|
|
201
|
+
return Number(text);
|
|
202
|
+
}
|
|
203
|
+
// array → starts with [, ends with ]
|
|
204
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
205
|
+
// Extract inner content and parse array elements
|
|
206
|
+
// Task 2.4 will implement full element parsing with quoting support
|
|
207
|
+
// For now, do a simple split respecting quoted elements
|
|
208
|
+
const inner = text.slice(1, -1).trim();
|
|
209
|
+
// Handle empty array
|
|
210
|
+
if (inner === "") {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
// Parse array elements (basic version, full implementation in task 2.4)
|
|
214
|
+
return parseArrayElements(inner);
|
|
215
|
+
}
|
|
216
|
+
// default → string
|
|
217
|
+
return text;
|
|
218
|
+
};
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Headline Encoder
|
|
221
|
+
// ============================================================================
|
|
222
|
+
/**
|
|
223
|
+
* Quotes a value for embedding in a headline.
|
|
224
|
+
* Wraps the value in double quotes and escapes any inner double quotes.
|
|
225
|
+
*
|
|
226
|
+
* @param value - The serialized value to quote
|
|
227
|
+
* @returns The quoted value with escaped inner quotes
|
|
228
|
+
*/
|
|
229
|
+
const quoteValue = (value) => {
|
|
230
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Encodes a record into a headline string using a compiled template.
|
|
234
|
+
* Substitutes field values into the template, emitting literals verbatim.
|
|
235
|
+
* For non-last fields, if the serialized value contains the next literal
|
|
236
|
+
* delimiter, the value is quoted to prevent parsing ambiguity.
|
|
237
|
+
*
|
|
238
|
+
* @param record - The record object with field values
|
|
239
|
+
* @param template - The compiled template with segments and fields
|
|
240
|
+
* @returns The encoded headline string
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const template = compileTemplate('#{id} "{title}" by {author}')
|
|
245
|
+
* const record = { id: "1", title: "Dune", author: "Frank Herbert" }
|
|
246
|
+
* encodeHeadline(record, template)
|
|
247
|
+
* // → '#1 "Dune" by Frank Herbert'
|
|
248
|
+
*
|
|
249
|
+
* // When value contains the next delimiter:
|
|
250
|
+
* const record2 = { id: "1", title: 'Say "hello"', author: "Test" }
|
|
251
|
+
* encodeHeadline(record2, template)
|
|
252
|
+
* // → '#1 "Say \"hello\"" by Test'
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export const encodeHeadline = (record, template) => {
|
|
256
|
+
let result = "";
|
|
257
|
+
const { segments } = template;
|
|
258
|
+
for (let i = 0; i < segments.length; i++) {
|
|
259
|
+
const segment = segments[i];
|
|
260
|
+
if (segment.type === "literal") {
|
|
261
|
+
result += segment.text;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Field segment - serialize the value
|
|
265
|
+
const value = record[segment.name];
|
|
266
|
+
const serialized = serializeValue(value);
|
|
267
|
+
// Find the next literal after this field (if any)
|
|
268
|
+
const nextLiteral = findNextLiteral(segments, i);
|
|
269
|
+
// If this is not the last field (has a subsequent literal delimiter)
|
|
270
|
+
// and the serialized value contains that delimiter, quote it
|
|
271
|
+
if (nextLiteral !== null && serialized.includes(nextLiteral)) {
|
|
272
|
+
result += quoteValue(serialized);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
result += serialized;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Finds the next literal text after a given segment index.
|
|
283
|
+
* Returns null if there is no subsequent literal (meaning this is the last field).
|
|
284
|
+
*
|
|
285
|
+
* @param segments - The template segments
|
|
286
|
+
* @param currentIndex - The current segment index
|
|
287
|
+
* @returns The next literal text, or null if none exists
|
|
288
|
+
*/
|
|
289
|
+
const findNextLiteral = (segments, currentIndex) => {
|
|
290
|
+
for (let i = currentIndex + 1; i < segments.length; i++) {
|
|
291
|
+
if (segments[i].type === "literal") {
|
|
292
|
+
return segments[i].text;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// Headline Decoder
|
|
299
|
+
// ============================================================================
|
|
300
|
+
/**
|
|
301
|
+
* Decodes a headline string back to a record using a compiled template.
|
|
302
|
+
* Performs a left-to-right scan matching literals and capturing field text between them.
|
|
303
|
+
* Returns null if the line doesn't match the template structure.
|
|
304
|
+
*
|
|
305
|
+
* @param line - The headline string to decode
|
|
306
|
+
* @param template - The compiled template with segments and fields
|
|
307
|
+
* @returns The decoded record object, or null if the line doesn't match
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* const template = compileTemplate('#{id} "{title}" by {author}')
|
|
312
|
+
* decodeHeadline('#1 "Dune" by Frank Herbert', template)
|
|
313
|
+
* // → { id: "1", title: "Dune", author: "Frank Herbert" }
|
|
314
|
+
*
|
|
315
|
+
* decodeHeadline('This does not match', template)
|
|
316
|
+
* // → null
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
export const decodeHeadline = (line, template) => {
|
|
320
|
+
const { segments } = template;
|
|
321
|
+
const result = {};
|
|
322
|
+
let pos = 0;
|
|
323
|
+
for (let i = 0; i < segments.length; i++) {
|
|
324
|
+
const segment = segments[i];
|
|
325
|
+
if (segment.type === "literal") {
|
|
326
|
+
// Check if the literal matches at the current position
|
|
327
|
+
if (!line.startsWith(segment.text, pos)) {
|
|
328
|
+
return null; // No match
|
|
329
|
+
}
|
|
330
|
+
pos += segment.text.length;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Field segment - capture text until the next literal (or end of line)
|
|
334
|
+
const nextLiteralText = findNextLiteralText(segments, i);
|
|
335
|
+
let fieldValue;
|
|
336
|
+
if (nextLiteralText === null) {
|
|
337
|
+
// This is the last field - greedy capture to end of line
|
|
338
|
+
fieldValue = line.slice(pos);
|
|
339
|
+
pos = line.length;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Find the next literal delimiter, respecting quoted values
|
|
343
|
+
const captureResult = captureFieldValue(line, pos, nextLiteralText);
|
|
344
|
+
if (captureResult === null) {
|
|
345
|
+
return null; // Delimiter not found, no match
|
|
346
|
+
}
|
|
347
|
+
fieldValue = captureResult.value;
|
|
348
|
+
pos = captureResult.endPos;
|
|
349
|
+
}
|
|
350
|
+
// Deserialize the captured field value
|
|
351
|
+
result[segment.name] = deserializeFieldValue(fieldValue);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Ensure we consumed the entire line
|
|
355
|
+
if (pos !== line.length) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Finds the text of the next literal segment after a given index.
|
|
362
|
+
* Returns null if there is no subsequent literal.
|
|
363
|
+
*/
|
|
364
|
+
const findNextLiteralText = (segments, currentIndex) => {
|
|
365
|
+
for (let i = currentIndex + 1; i < segments.length; i++) {
|
|
366
|
+
if (segments[i].type === "literal") {
|
|
367
|
+
return segments[i].text;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Captures a field value from the line, handling quoted values.
|
|
374
|
+
* If the value starts with a quote, scans for the closing quote (respecting `\"` escapes).
|
|
375
|
+
* Otherwise, scans for the next occurrence of the delimiter.
|
|
376
|
+
*
|
|
377
|
+
* The endPos returned is the position where the delimiter starts (not after it),
|
|
378
|
+
* so the calling loop can process the literal segment normally.
|
|
379
|
+
*
|
|
380
|
+
* @param line - The full line being parsed
|
|
381
|
+
* @param startPos - The starting position for capture
|
|
382
|
+
* @param delimiter - The literal delimiter to find
|
|
383
|
+
* @returns The captured value and position where delimiter starts, or null if not found
|
|
384
|
+
*/
|
|
385
|
+
const captureFieldValue = (line, startPos, delimiter) => {
|
|
386
|
+
// Check if the field value is quoted
|
|
387
|
+
if (line[startPos] === '"') {
|
|
388
|
+
// Quoted value - scan for closing quote respecting escapes
|
|
389
|
+
const quoteResult = scanQuotedValue(line, startPos);
|
|
390
|
+
if (quoteResult === null) {
|
|
391
|
+
return null; // Unclosed quote
|
|
392
|
+
}
|
|
393
|
+
// After the quoted value, expect the delimiter
|
|
394
|
+
if (!line.startsWith(delimiter, quoteResult.endPos)) {
|
|
395
|
+
return null; // Delimiter not found after quoted value
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
value: quoteResult.value,
|
|
399
|
+
endPos: quoteResult.endPos, // Position after closing quote, before delimiter
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Unquoted value - find the next occurrence of the delimiter
|
|
403
|
+
const delimiterPos = line.indexOf(delimiter, startPos);
|
|
404
|
+
if (delimiterPos === -1) {
|
|
405
|
+
return null; // Delimiter not found
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
value: line.slice(startPos, delimiterPos),
|
|
409
|
+
endPos: delimiterPos, // Position where delimiter starts, not ends
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Scans a quoted value starting at the given position.
|
|
414
|
+
* Handles escaped quotes (\" inside the quoted string).
|
|
415
|
+
*
|
|
416
|
+
* @param line - The line being parsed
|
|
417
|
+
* @param startPos - The position of the opening quote
|
|
418
|
+
* @returns The unquoted, unescaped value and the position after the closing quote
|
|
419
|
+
*/
|
|
420
|
+
const scanQuotedValue = (line, startPos) => {
|
|
421
|
+
// Skip the opening quote
|
|
422
|
+
let pos = startPos + 1;
|
|
423
|
+
let value = "";
|
|
424
|
+
while (pos < line.length) {
|
|
425
|
+
const char = line[pos];
|
|
426
|
+
if (char === "\\") {
|
|
427
|
+
// Escape sequence - check the next character
|
|
428
|
+
if (pos + 1 < line.length) {
|
|
429
|
+
const nextChar = line[pos + 1];
|
|
430
|
+
if (nextChar === '"') {
|
|
431
|
+
// Escaped quote - add the quote to value
|
|
432
|
+
value += '"';
|
|
433
|
+
pos += 2;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Not an escape sequence, just a backslash
|
|
438
|
+
value += char;
|
|
439
|
+
pos++;
|
|
440
|
+
}
|
|
441
|
+
else if (char === '"') {
|
|
442
|
+
// Closing quote found
|
|
443
|
+
return { value, endPos: pos + 1 };
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
value += char;
|
|
447
|
+
pos++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Unclosed quote
|
|
451
|
+
return null;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Deserializes a field value captured from a headline.
|
|
455
|
+
* This is the same as deserializeValue but handles already-unquoted values.
|
|
456
|
+
*
|
|
457
|
+
* @param fieldValue - The raw field value string
|
|
458
|
+
* @returns The deserialized value
|
|
459
|
+
*/
|
|
460
|
+
const deserializeFieldValue = (fieldValue) => {
|
|
461
|
+
return deserializeValue(fieldValue);
|
|
462
|
+
};
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// Overflow Encoder
|
|
465
|
+
// ============================================================================
|
|
466
|
+
/**
|
|
467
|
+
* Default indentation for overflow lines.
|
|
468
|
+
*/
|
|
469
|
+
const OVERFLOW_INDENT = " ";
|
|
470
|
+
/**
|
|
471
|
+
* Deeper indentation for continuation lines (multi-line values).
|
|
472
|
+
*/
|
|
473
|
+
const CONTINUATION_INDENT = " ";
|
|
474
|
+
/**
|
|
475
|
+
* Checks if any field value in a record contains newlines for the given fields.
|
|
476
|
+
*
|
|
477
|
+
* @param record - The record to check
|
|
478
|
+
* @param fields - The field names to check
|
|
479
|
+
* @returns The first field name with a multi-line value, or null if none
|
|
480
|
+
*/
|
|
481
|
+
const findMultiLineField = (record, fields) => {
|
|
482
|
+
for (const fieldName of fields) {
|
|
483
|
+
const value = record[fieldName];
|
|
484
|
+
if (typeof value === "string" && value.includes("\n")) {
|
|
485
|
+
return fieldName;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Encodes a record with multi-line field handling.
|
|
492
|
+
* If the field value contains newlines, the first line goes on the template line
|
|
493
|
+
* and subsequent lines become continuation lines with deeper indentation.
|
|
494
|
+
*
|
|
495
|
+
* @param record - The record object with field values
|
|
496
|
+
* @param template - The compiled template
|
|
497
|
+
* @param multiLineField - The field name that contains multi-line content
|
|
498
|
+
* @returns Array of line strings: first line is the overflow line, rest are continuation lines
|
|
499
|
+
*/
|
|
500
|
+
const encodeMultiLineOverflow = (record, template, multiLineField) => {
|
|
501
|
+
const value = record[multiLineField];
|
|
502
|
+
if (typeof value !== "string") {
|
|
503
|
+
// Should not happen, but handle gracefully
|
|
504
|
+
return [OVERFLOW_INDENT + encodeHeadline(record, template)];
|
|
505
|
+
}
|
|
506
|
+
const valueLines = value.split("\n");
|
|
507
|
+
const firstLineValue = valueLines[0];
|
|
508
|
+
// Create a modified record with only the first line of the multi-line value
|
|
509
|
+
const modifiedRecord = {
|
|
510
|
+
...record,
|
|
511
|
+
[multiLineField]: firstLineValue,
|
|
512
|
+
};
|
|
513
|
+
const lines = [];
|
|
514
|
+
// First line: the overflow template with first line of value
|
|
515
|
+
lines.push(OVERFLOW_INDENT + encodeHeadline(modifiedRecord, template));
|
|
516
|
+
// Continuation lines: deeper indented
|
|
517
|
+
for (let i = 1; i < valueLines.length; i++) {
|
|
518
|
+
lines.push(CONTINUATION_INDENT + valueLines[i]);
|
|
519
|
+
}
|
|
520
|
+
return lines;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* Encodes overflow fields for a record as indented lines.
|
|
524
|
+
* For each overflow template, if the record has a non-null/non-undefined value
|
|
525
|
+
* for the field in that template, emits an indented line using the template.
|
|
526
|
+
* Overflow fields with null or undefined values are omitted.
|
|
527
|
+
*
|
|
528
|
+
* For multi-line string values (containing newlines), the first line is encoded
|
|
529
|
+
* on the template line, and subsequent lines are emitted as continuation lines
|
|
530
|
+
* with deeper indentation.
|
|
531
|
+
*
|
|
532
|
+
* @param record - The record object with field values
|
|
533
|
+
* @param overflowTemplates - Array of compiled overflow templates
|
|
534
|
+
* @returns Array of indented overflow line strings
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```typescript
|
|
538
|
+
* const templates = compileOverflowTemplates(['tagged {tags}', '~ {description}'])
|
|
539
|
+
* const record = { id: "1", title: "Dune", tags: ["classic"], description: null }
|
|
540
|
+
* encodeOverflowLines(record, templates)
|
|
541
|
+
* // → [' tagged [classic]']
|
|
542
|
+
* // Note: description is null, so its overflow line is omitted
|
|
543
|
+
*
|
|
544
|
+
* // Multi-line value:
|
|
545
|
+
* const record2 = { id: "1", description: "Line one\nLine two" }
|
|
546
|
+
* encodeOverflowLines(record2, compileOverflowTemplates(['~ {description}']))
|
|
547
|
+
* // → [' ~ Line one', ' Line two']
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
export const encodeOverflowLines = (record, overflowTemplates) => {
|
|
551
|
+
const lines = [];
|
|
552
|
+
for (const template of overflowTemplates) {
|
|
553
|
+
// Check if any field in this template has a non-null value
|
|
554
|
+
// Overflow templates typically have a single field, but we support multiple
|
|
555
|
+
const hasNonNullValue = template.fields.some((fieldName) => {
|
|
556
|
+
const value = record[fieldName];
|
|
557
|
+
return value !== null && value !== undefined;
|
|
558
|
+
});
|
|
559
|
+
if (hasNonNullValue) {
|
|
560
|
+
// Check for multi-line field values
|
|
561
|
+
const multiLineField = findMultiLineField(record, template.fields);
|
|
562
|
+
if (multiLineField !== null) {
|
|
563
|
+
// Handle multi-line value with continuation lines
|
|
564
|
+
const overflowLines = encodeMultiLineOverflow(record, template, multiLineField);
|
|
565
|
+
lines.push(...overflowLines);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Single-line value: encode normally
|
|
569
|
+
const overflowLine = encodeHeadline(record, template);
|
|
570
|
+
lines.push(OVERFLOW_INDENT + overflowLine);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return lines;
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* Measures the indentation level of a line (number of leading spaces/tabs).
|
|
578
|
+
*
|
|
579
|
+
* @param line - The line to measure
|
|
580
|
+
* @returns The number of leading whitespace characters
|
|
581
|
+
*/
|
|
582
|
+
const measureIndent = (line) => {
|
|
583
|
+
let indent = 0;
|
|
584
|
+
for (const char of line) {
|
|
585
|
+
if (char === " " || char === "\t") {
|
|
586
|
+
indent++;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return indent;
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Decodes overflow lines for a record using the configured overflow templates.
|
|
596
|
+
* Collects indented lines belonging to the record, tries each overflow template
|
|
597
|
+
* in order, skips on non-match, and captures field values on match.
|
|
598
|
+
*
|
|
599
|
+
* For each indented line:
|
|
600
|
+
* 1. Try matching against each overflow template (in order)
|
|
601
|
+
* 2. If a template matches, capture the field values and move to next line
|
|
602
|
+
* 3. If no template matches, check if it's a continuation line (deeper indentation)
|
|
603
|
+
* 4. Continuation lines are appended to the previous field's value with newline
|
|
604
|
+
*
|
|
605
|
+
* @param lines - Array of indented lines (already collected for this record)
|
|
606
|
+
* @param overflowTemplates - Array of compiled overflow templates
|
|
607
|
+
* @param baseIndent - The expected indentation level for overflow lines (default: 2)
|
|
608
|
+
* @returns The decoded field values and number of lines consumed
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```typescript
|
|
612
|
+
* const templates = compileOverflowTemplates(['tagged {tags}', '~ {description}'])
|
|
613
|
+
* const lines = [' tagged [sci-fi]', ' ~ A classic novel']
|
|
614
|
+
* const result = decodeOverflowLines(lines, templates)
|
|
615
|
+
* // → { fields: { tags: ['sci-fi'], description: 'A classic novel' }, linesConsumed: 2 }
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
export const decodeOverflowLines = (lines, overflowTemplates, baseIndent = 2) => {
|
|
619
|
+
const fields = {};
|
|
620
|
+
let lineIndex = 0;
|
|
621
|
+
let lastMatchedField = null;
|
|
622
|
+
while (lineIndex < lines.length) {
|
|
623
|
+
const line = lines[lineIndex];
|
|
624
|
+
const indent = measureIndent(line);
|
|
625
|
+
// Check if line is indented enough to be part of this record's overflow
|
|
626
|
+
if (indent < baseIndent) {
|
|
627
|
+
// Line is not indented enough, stop processing
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
// Check if this is a continuation line (deeper indentation than base)
|
|
631
|
+
if (indent > baseIndent && lastMatchedField !== null) {
|
|
632
|
+
// Continuation line - append to the last matched field
|
|
633
|
+
const existingValue = fields[lastMatchedField];
|
|
634
|
+
const continuationContent = line.slice(indent); // Strip all leading whitespace
|
|
635
|
+
if (typeof existingValue === "string") {
|
|
636
|
+
fields[lastMatchedField] = existingValue + "\n" + continuationContent;
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// Shouldn't happen in well-formed input, but handle it
|
|
640
|
+
fields[lastMatchedField] = String(existingValue) + "\n" + continuationContent;
|
|
641
|
+
}
|
|
642
|
+
lineIndex++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
// Strip the base indentation to get the content
|
|
646
|
+
const content = line.slice(baseIndent);
|
|
647
|
+
// Try each overflow template in order
|
|
648
|
+
let matched = false;
|
|
649
|
+
for (const template of overflowTemplates) {
|
|
650
|
+
const decoded = decodeHeadline(content, template);
|
|
651
|
+
if (decoded !== null) {
|
|
652
|
+
// Template matched - merge the decoded fields
|
|
653
|
+
for (const [fieldName, value] of Object.entries(decoded)) {
|
|
654
|
+
fields[fieldName] = value;
|
|
655
|
+
// Track the last matched field for continuation lines
|
|
656
|
+
// (typically the last field in the template, often the only one)
|
|
657
|
+
lastMatchedField = fieldName;
|
|
658
|
+
}
|
|
659
|
+
matched = true;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!matched) {
|
|
664
|
+
// No template matched - this could be:
|
|
665
|
+
// 1. A malformed overflow line (skip it)
|
|
666
|
+
// 2. Or we're past the record's overflow section
|
|
667
|
+
// For robustness, we skip and continue trying
|
|
668
|
+
// If it's deeply indented, it might be a continuation without a prior match
|
|
669
|
+
if (indent > baseIndent && lastMatchedField !== null) {
|
|
670
|
+
// Treat as continuation anyway
|
|
671
|
+
const existingValue = fields[lastMatchedField];
|
|
672
|
+
const continuationContent = line.slice(indent);
|
|
673
|
+
if (typeof existingValue === "string") {
|
|
674
|
+
fields[lastMatchedField] = existingValue + "\n" + continuationContent;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// If no match and no prior field, we just skip this line
|
|
678
|
+
}
|
|
679
|
+
lineIndex++;
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
fields,
|
|
683
|
+
linesConsumed: lineIndex,
|
|
684
|
+
};
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Scans a document for the @prose directive.
|
|
688
|
+
* The directive is a line starting with `@prose ` (note the trailing space).
|
|
689
|
+
*
|
|
690
|
+
* Rules:
|
|
691
|
+
* - Exactly one @prose directive must exist in the file
|
|
692
|
+
* - If no directive is found, throws an error
|
|
693
|
+
* - If multiple directives are found, throws an error
|
|
694
|
+
* - All lines before the directive are preamble
|
|
695
|
+
*
|
|
696
|
+
* @param lines - Array of lines from the document
|
|
697
|
+
* @returns The position information for preamble and directive
|
|
698
|
+
* @throws Error if no directive found or multiple directives found
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* ```typescript
|
|
702
|
+
* const lines = ['# My Books', '', '@prose #{id} {title}', '#1 Dune']
|
|
703
|
+
* const result = scanDirective(lines)
|
|
704
|
+
* // → { preambleEnd: 1, directiveStart: 2 }
|
|
705
|
+
*
|
|
706
|
+
* const linesNoPreable = ['@prose #{id} {title}', '#1 Dune']
|
|
707
|
+
* const result2 = scanDirective(linesNoPreable)
|
|
708
|
+
* // → { preambleEnd: -1, directiveStart: 0 }
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
export const scanDirective = (lines) => {
|
|
712
|
+
let directiveIndex = null;
|
|
713
|
+
for (let i = 0; i < lines.length; i++) {
|
|
714
|
+
const line = lines[i];
|
|
715
|
+
// Check if this line starts with "@prose "
|
|
716
|
+
if (line.startsWith("@prose ")) {
|
|
717
|
+
if (directiveIndex !== null) {
|
|
718
|
+
// Multiple directives found
|
|
719
|
+
throw new Error(`Multiple @prose directives found: first at line ${directiveIndex + 1}, second at line ${i + 1}. Only one directive per file is allowed.`);
|
|
720
|
+
}
|
|
721
|
+
directiveIndex = i;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (directiveIndex === null) {
|
|
725
|
+
throw new Error("No @prose directive found. The file must contain a line starting with '@prose ' to define the record template.");
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
preambleEnd: directiveIndex > 0 ? directiveIndex - 1 : -1,
|
|
729
|
+
directiveStart: directiveIndex,
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* Parses a directive block from the document.
|
|
734
|
+
* Extracts the headline template from the @prose line and collects
|
|
735
|
+
* any indented overflow templates that immediately follow.
|
|
736
|
+
*
|
|
737
|
+
* The directive block structure:
|
|
738
|
+
* ```
|
|
739
|
+
* @prose #{id} "{title}" by {author} ← headline template
|
|
740
|
+
* tagged {tags} ← overflow template 1
|
|
741
|
+
* ~ {description} ← overflow template 2
|
|
742
|
+
* ← blank line or non-indented = end of block
|
|
743
|
+
* ```
|
|
744
|
+
*
|
|
745
|
+
* Overflow templates are lines that:
|
|
746
|
+
* - Immediately follow the @prose line (no blank lines between)
|
|
747
|
+
* - Are indented (start with whitespace)
|
|
748
|
+
*
|
|
749
|
+
* @param lines - Array of lines from the document
|
|
750
|
+
* @param directiveStart - Index of the @prose directive line
|
|
751
|
+
* @returns The parsed directive block with template strings and body start index
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
755
|
+
* const lines = [
|
|
756
|
+
* '@prose #{id} "{title}"',
|
|
757
|
+
* ' tagged {tags}',
|
|
758
|
+
* ' ~ {description}',
|
|
759
|
+
* '',
|
|
760
|
+
* '#1 "Dune"',
|
|
761
|
+
* ]
|
|
762
|
+
* const result = parseDirectiveBlock(lines, 0)
|
|
763
|
+
* // → {
|
|
764
|
+
* // headlineTemplate: '#{id} "{title}"',
|
|
765
|
+
* // overflowTemplates: ['tagged {tags}', '~ {description}'],
|
|
766
|
+
* // bodyStart: 3
|
|
767
|
+
* // }
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
export const parseDirectiveBlock = (lines, directiveStart) => {
|
|
771
|
+
const directiveLine = lines[directiveStart];
|
|
772
|
+
// Extract headline template: everything after "@prose "
|
|
773
|
+
const headlineTemplate = directiveLine.slice("@prose ".length);
|
|
774
|
+
// Collect overflow templates: indented lines immediately following
|
|
775
|
+
const overflowTemplates = [];
|
|
776
|
+
let lineIndex = directiveStart + 1;
|
|
777
|
+
while (lineIndex < lines.length) {
|
|
778
|
+
const line = lines[lineIndex];
|
|
779
|
+
// Check if line is indented (starts with whitespace)
|
|
780
|
+
if (line.length > 0 && (line[0] === " " || line[0] === "\t")) {
|
|
781
|
+
// This is an overflow template - strip leading whitespace
|
|
782
|
+
const templateContent = line.trimStart();
|
|
783
|
+
overflowTemplates.push(templateContent);
|
|
784
|
+
lineIndex++;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
// Not indented or empty line - end of directive block
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
headlineTemplate,
|
|
793
|
+
overflowTemplates,
|
|
794
|
+
bodyStart: lineIndex,
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
/**
|
|
798
|
+
* Parses the body section of a prose document.
|
|
799
|
+
* Iterates lines after the directive block and classifies each as:
|
|
800
|
+
* - Record headline (matches the compiled template)
|
|
801
|
+
* - Indented overflow/continuation (part of the current record)
|
|
802
|
+
* - Pass-through text (doesn't match, preserved verbatim)
|
|
803
|
+
*
|
|
804
|
+
* @param lines - Array of lines from the document
|
|
805
|
+
* @param bodyStart - Index of the first line of the body (after directive block)
|
|
806
|
+
* @param headlineTemplate - The compiled headline template
|
|
807
|
+
* @returns The parsed body with interleaved records and pass-through text
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* ```typescript
|
|
811
|
+
* const lines = [
|
|
812
|
+
* '@prose #{id} "{title}"',
|
|
813
|
+
* '',
|
|
814
|
+
* '## Science Fiction',
|
|
815
|
+
* '#1 "Dune"',
|
|
816
|
+
* ' tagged [classic]',
|
|
817
|
+
* '#2 "Neuromancer"',
|
|
818
|
+
* '',
|
|
819
|
+
* '## Fantasy',
|
|
820
|
+
* '#3 "The Hobbit"',
|
|
821
|
+
* ]
|
|
822
|
+
* const template = compileTemplate('#{id} "{title}"')
|
|
823
|
+
* const result = parseBody(lines, 1, template)
|
|
824
|
+
* // → {
|
|
825
|
+
* // entries: [
|
|
826
|
+
* // { type: "passthrough", lines: ["", "## Science Fiction"] },
|
|
827
|
+
* // { type: "record", fields: { id: "1", title: "Dune" }, headline: '#1 "Dune"', overflowLines: [" tagged [classic]"] },
|
|
828
|
+
* // { type: "record", fields: { id: "2", title: "Neuromancer" }, headline: '#2 "Neuromancer"', overflowLines: [] },
|
|
829
|
+
* // { type: "passthrough", lines: ["", "## Fantasy"] },
|
|
830
|
+
* // { type: "record", fields: { id: "3", title: "The Hobbit" }, headline: '#3 "The Hobbit"', overflowLines: [] },
|
|
831
|
+
* // ]
|
|
832
|
+
* // }
|
|
833
|
+
* ```
|
|
834
|
+
*/
|
|
835
|
+
export const parseBody = (lines, bodyStart, headlineTemplate) => {
|
|
836
|
+
const entries = [];
|
|
837
|
+
let lineIndex = bodyStart;
|
|
838
|
+
let currentPassthrough = [];
|
|
839
|
+
// Helper to flush accumulated pass-through lines into an entry
|
|
840
|
+
const flushPassthrough = () => {
|
|
841
|
+
if (currentPassthrough.length > 0) {
|
|
842
|
+
entries.push({
|
|
843
|
+
type: "passthrough",
|
|
844
|
+
lines: [...currentPassthrough],
|
|
845
|
+
});
|
|
846
|
+
currentPassthrough = [];
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
while (lineIndex < lines.length) {
|
|
850
|
+
const line = lines[lineIndex];
|
|
851
|
+
// Check if this line is indented (starts with whitespace)
|
|
852
|
+
if (line.length > 0 && (line[0] === " " || line[0] === "\t")) {
|
|
853
|
+
// Indented line — belongs to the previous record's overflow
|
|
854
|
+
// If we have a current record (last entry is a record), add to its overflow
|
|
855
|
+
// Otherwise, treat as pass-through (malformed input)
|
|
856
|
+
const lastEntry = entries[entries.length - 1];
|
|
857
|
+
if (lastEntry && lastEntry.type === "record") {
|
|
858
|
+
// Add to the record's overflow lines (we need to mutate, so cast)
|
|
859
|
+
lastEntry.overflowLines.push(line);
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
// No record to attach to — treat as pass-through
|
|
863
|
+
currentPassthrough.push(line);
|
|
864
|
+
}
|
|
865
|
+
lineIndex++;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
// Not indented — try to match against the headline template
|
|
869
|
+
const decoded = decodeHeadline(line, headlineTemplate);
|
|
870
|
+
if (decoded !== null) {
|
|
871
|
+
// Line matches the template — it's a record headline
|
|
872
|
+
// First, flush any accumulated pass-through
|
|
873
|
+
flushPassthrough();
|
|
874
|
+
// Create a new record entry
|
|
875
|
+
entries.push({
|
|
876
|
+
type: "record",
|
|
877
|
+
fields: decoded,
|
|
878
|
+
headline: line,
|
|
879
|
+
overflowLines: [],
|
|
880
|
+
});
|
|
881
|
+
lineIndex++;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
// Line doesn't match — it's pass-through text
|
|
885
|
+
currentPassthrough.push(line);
|
|
886
|
+
lineIndex++;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Flush any remaining pass-through lines
|
|
890
|
+
flushPassthrough();
|
|
891
|
+
return { entries };
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* Compiles prose codec options into internal state.
|
|
895
|
+
* Called once at codec construction time.
|
|
896
|
+
*
|
|
897
|
+
* @param options - The prose codec options
|
|
898
|
+
* @returns The compiled codec state
|
|
899
|
+
*/
|
|
900
|
+
const compileProseCodecOptions = (options) => {
|
|
901
|
+
const headlineTemplate = compileTemplate(options.template);
|
|
902
|
+
const overflowTemplates = compileOverflowTemplates(options.overflow);
|
|
903
|
+
return {
|
|
904
|
+
headlineTemplate,
|
|
905
|
+
overflowTemplates,
|
|
906
|
+
rawHeadlineTemplate: options.template,
|
|
907
|
+
rawOverflowTemplates: options.overflow ?? [],
|
|
908
|
+
};
|
|
909
|
+
};
|
|
910
|
+
/**
|
|
911
|
+
* Creates a prose format codec for human-readable, template-driven serialization.
|
|
912
|
+
*
|
|
913
|
+
* The prose format uses a `@prose` directive to define a sentence-like pattern
|
|
914
|
+
* mapping field names to positions within literal delimiter text. Records follow
|
|
915
|
+
* this pattern, producing human-readable lines.
|
|
916
|
+
*
|
|
917
|
+
* Templates use `{fieldName}` placeholders mixed with literal text:
|
|
918
|
+
* ```
|
|
919
|
+
* @prose #{id} "{title}" by {authorId} ({year}) — {genre}
|
|
920
|
+
* tagged {tags}
|
|
921
|
+
* ~ {description}
|
|
922
|
+
* ```
|
|
923
|
+
*
|
|
924
|
+
* The codec compiles templates at construction time and returns a standard
|
|
925
|
+
* FormatCodec with encode/decode functions.
|
|
926
|
+
*
|
|
927
|
+
* @param options - Codec configuration with headline and overflow templates
|
|
928
|
+
* @param options.template - The headline template with {fieldName} placeholders
|
|
929
|
+
* @param options.overflow - Optional array of overflow templates for additional fields
|
|
930
|
+
* @returns A FormatCodec for prose serialization
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```typescript
|
|
934
|
+
* const codec = proseCodec({
|
|
935
|
+
* template: '#{id} "{title}" by {author}',
|
|
936
|
+
* overflow: ['tagged {tags}', '~ {description}'],
|
|
937
|
+
* })
|
|
938
|
+
*
|
|
939
|
+
* const layer = makeSerializerLayer([codec])
|
|
940
|
+
*
|
|
941
|
+
* // Encoded output:
|
|
942
|
+
* // @prose #{id} "{title}" by {author}
|
|
943
|
+
* // tagged {tags}
|
|
944
|
+
* // ~ {description}
|
|
945
|
+
* //
|
|
946
|
+
* // #1 "Dune" by Frank Herbert
|
|
947
|
+
* // tagged [sci-fi, classic]
|
|
948
|
+
* // ~ A masterpiece of science fiction
|
|
949
|
+
* ```
|
|
950
|
+
*/
|
|
951
|
+
export const proseCodec = (options) => {
|
|
952
|
+
// Compile templates at construction time
|
|
953
|
+
const compiled = compileProseCodecOptions(options);
|
|
954
|
+
return {
|
|
955
|
+
name: "prose",
|
|
956
|
+
extensions: ["prose"],
|
|
957
|
+
encode: (data, _formatOptions) => {
|
|
958
|
+
if (!Array.isArray(data)) {
|
|
959
|
+
throw new Error("Prose codec expects an array of records to encode");
|
|
960
|
+
}
|
|
961
|
+
const lines = [];
|
|
962
|
+
// Write the @prose directive
|
|
963
|
+
lines.push(`@prose ${compiled.rawHeadlineTemplate}`);
|
|
964
|
+
// Write overflow template declarations (indented)
|
|
965
|
+
for (const overflowTemplate of compiled.rawOverflowTemplates) {
|
|
966
|
+
lines.push(` ${overflowTemplate}`);
|
|
967
|
+
}
|
|
968
|
+
// Blank line to separate directive block from body
|
|
969
|
+
lines.push("");
|
|
970
|
+
// Encode each record
|
|
971
|
+
for (const record of data) {
|
|
972
|
+
// Encode headline
|
|
973
|
+
const headline = encodeHeadline(record, compiled.headlineTemplate);
|
|
974
|
+
lines.push(headline);
|
|
975
|
+
// Encode overflow lines
|
|
976
|
+
const overflowLines = encodeOverflowLines(record, compiled.overflowTemplates);
|
|
977
|
+
lines.push(...overflowLines);
|
|
978
|
+
}
|
|
979
|
+
return lines.join("\n");
|
|
980
|
+
},
|
|
981
|
+
decode: (raw) => {
|
|
982
|
+
const lines = raw.split("\n");
|
|
983
|
+
// Scan for the directive
|
|
984
|
+
const scanResult = scanDirective(lines);
|
|
985
|
+
// Parse the directive block
|
|
986
|
+
const directiveBlock = parseDirectiveBlock(lines, scanResult.directiveStart);
|
|
987
|
+
// Compile the file's headline template for parsing
|
|
988
|
+
// Note: We use the file's template for decoding, ensuring self-describing files work
|
|
989
|
+
const fileHeadlineTemplate = compileTemplate(directiveBlock.headlineTemplate);
|
|
990
|
+
const fileOverflowTemplates = compileOverflowTemplates(directiveBlock.overflowTemplates);
|
|
991
|
+
// Parse the body
|
|
992
|
+
const bodyResult = parseBody(lines, directiveBlock.bodyStart, fileHeadlineTemplate);
|
|
993
|
+
// Extract records from entries, decoding overflow fields
|
|
994
|
+
const records = [];
|
|
995
|
+
for (const entry of bodyResult.entries) {
|
|
996
|
+
if (entry.type === "record") {
|
|
997
|
+
// Start with headline fields
|
|
998
|
+
const record = { ...entry.fields };
|
|
999
|
+
// Decode overflow lines to get additional fields
|
|
1000
|
+
if (entry.overflowLines.length > 0) {
|
|
1001
|
+
const overflowResult = decodeOverflowLines(entry.overflowLines, fileOverflowTemplates);
|
|
1002
|
+
// Merge overflow fields into the record
|
|
1003
|
+
for (const [fieldName, value] of Object.entries(overflowResult.fields)) {
|
|
1004
|
+
record[fieldName] = value;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
records.push(record);
|
|
1008
|
+
}
|
|
1009
|
+
// Pass-through entries are skipped (v1: not preserved through re-encode)
|
|
1010
|
+
}
|
|
1011
|
+
return records;
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
};
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
// Array Parsing Helpers
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
/**
|
|
1019
|
+
* Parses array element string into an array of deserialized values.
|
|
1020
|
+
* Handles quoted elements that may contain commas or brackets.
|
|
1021
|
+
*
|
|
1022
|
+
* @param inner - The content between [ and ]
|
|
1023
|
+
* @returns Array of deserialized values
|
|
1024
|
+
*/
|
|
1025
|
+
const parseArrayElements = (inner) => {
|
|
1026
|
+
const elements = [];
|
|
1027
|
+
let pos = 0;
|
|
1028
|
+
let elementStart = 0;
|
|
1029
|
+
let inQuotes = false;
|
|
1030
|
+
while (pos <= inner.length) {
|
|
1031
|
+
if (pos === inner.length || (!inQuotes && inner[pos] === ",")) {
|
|
1032
|
+
// Extract and trim the element
|
|
1033
|
+
const element = inner.slice(elementStart, pos).trim();
|
|
1034
|
+
if (element !== "") {
|
|
1035
|
+
// Handle quoted elements
|
|
1036
|
+
if (element.startsWith('"') && element.endsWith('"')) {
|
|
1037
|
+
// Remove quotes and unescape
|
|
1038
|
+
const unquoted = element.slice(1, -1).replace(/\\"/g, '"');
|
|
1039
|
+
elements.push(deserializeValue(unquoted));
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
elements.push(deserializeValue(element));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
elementStart = pos + 1;
|
|
1046
|
+
pos++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (inner[pos] === '"' && (pos === 0 || inner[pos - 1] !== "\\")) {
|
|
1050
|
+
// Check if this is at the start of an element (accounting for whitespace)
|
|
1051
|
+
const elementSoFar = inner.slice(elementStart, pos).trim();
|
|
1052
|
+
if (elementSoFar === "" || inQuotes) {
|
|
1053
|
+
inQuotes = !inQuotes;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
pos++;
|
|
1057
|
+
}
|
|
1058
|
+
return elements;
|
|
1059
|
+
};
|
|
1060
|
+
//# sourceMappingURL=prose.js.map
|