From e3c7554ff3bf7028dff7dc4bf5519b9f72a8b70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20L=C3=B6vqvist?= Date: Sun, 12 Apr 2026 01:12:15 +0200 Subject: [PATCH] Added basic-tree --- experiments/config2.mjs | 5 +- experiments/text-nodes.mjs | 157 ++++++++++++++++++ experiments/textnodes.mjs | 0 package-manifest.yaml | 11 +- source/data/field-configuration-factories.mjs | 30 ++++ source/text/basic-tree.mjs | 16 ++ 6 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 experiments/text-nodes.mjs delete mode 100644 experiments/textnodes.mjs create mode 100644 source/text/basic-tree.mjs diff --git a/experiments/config2.mjs b/experiments/config2.mjs index 637ccf6..885f19e 100644 --- a/experiments/config2.mjs +++ b/experiments/config2.mjs @@ -2,8 +2,9 @@ import * as F from '@efforting.tech/data/field-configuration-factories'; const s = new F.Schema({ foo: F.value(123, 'The value'), - bar: F.factory((t) => `Field ${t.field_name} was not set`, 'The factory'), -}); + bar: F.factory((t) => `Field ${t.name} was not set`, 'The factory'), + rq: F.required('Anything. Just not nothing.'), +}, 'Some schema'); console.log(s.load()) // { foo: 123, bar: 'Field bar was not set' } diff --git a/experiments/text-nodes.mjs b/experiments/text-nodes.mjs new file mode 100644 index 0000000..d05c907 --- /dev/null +++ b/experiments/text-nodes.mjs @@ -0,0 +1,157 @@ +import * as CF from '@efforting.tech/data/field-configuration-factories'; + +import { readFileSync } from 'node:fs'; +import { inspect } from 'node:util'; + +import { Indention_Mode, Text_Settings } from '@efforting.tech/text/basic-tree'; + +// TODO: Move into string helper module +function string_has_contents(str) { + return /\S/.test(str); +} + +function *indented_line_iterator(settings, text) { + + let line_no = settings.first_line; + let index = 0; + const { indention_tabulator_width } = settings; + + switch (settings.indention_mode) { + case Indention_Mode.symbols.TABULATORS: { + for (const line of text.matchAll(/^(\t*)(.*)$/gm)) { + const [raw, tabs, remaining] = line; + yield { raw, indent: tabs.length, line: remaining, line_no: line_no++, index: index++}; + } + break; + } + case Indention_Mode.symbols.SPACES: { + for (const line of text.matchAll(/^([ ]*)(.*)$/gm)) { + const [raw, spaces, remaining] = line; + + if ((spaces.length % indention_tabulator_width) !== 0) { + throw new Error('Unaligned indention'); //TODO - proper error + } + yield { raw, indent: Math.floor(spaces.length / indention_tabulator_width), line: remaining, line_no: line_no++, index: index++}; + } + break; + } + default: + throw new Error('Unsupported indention mode'); //TODO - proper error + + } + + +} + + +const ts = Text_Settings.load({ + indention_mode: 'TABULATORS', +}); + +const example_string = +`branch1 + leaf1 + leaf2 + +branch2 + sub-branch1 + leaf3 + leaf4 + + + sub-branch2 + leaf5 + +branch3 + dual-indented + +`; + + +class Text_Node { + constructor(text_settings, line=undefined, indent=0, line_no=undefined, index=undefined, raw=undefined, parent=undefined) { + Object.assign(this, { text_settings, line, indent, line_no, index, raw, parent, children: [] }); + } +} + + +const root = new Text_Node(ts); + +// NOTE: This first Text_Node is not added to the tree, it serves as an initial cursor only. +let current = new Text_Node(root.text_settings, undefined, 0, undefined, undefined, undefined, root); + + +for (const line_info of indented_line_iterator(ts, example_string)) { + + // TODO: Implement other variants than inherit-from-previous + const indent = string_has_contents(line_info.line) ? line_info.indent : current.indent; + + const delta_indent = indent - current.indent; + + if (delta_indent == 0) { + const pending = new Text_Node(current.text_settings, undefined, current.indent, undefined, undefined, undefined, current.parent); // Partial insertion - same level + if (current.parent) { + current.parent.children.push(pending); + } + current = pending; + } else if (delta_indent > 0) { + for (let i=0; idelta_indent; i--) { + current = current.parent; + } + + const pending = new Text_Node(current.text_settings, undefined, current.indent, undefined, undefined, undefined, current.parent); // Partial insertion - same level + if (current.parent) { + current.parent.children.push(pending); + } + current = pending; + } + + // Fill in partial insertion + Object.assign(current, { + line: line_info.line, + line_no: line_info.line_no, + index: line_info.index, + raw: line_info.raw, + }); + + +} + + +function debug_dump(node, level=0) { + console.log(`${" ".repeat(level)}[${node.line_no ?? '-'}] ${inspect(node.line)}`); + for (const child of node.children) { + debug_dump(child, level+1); + } +} + +debug_dump(root); +/* + +[-] undefined + [1] 'branch1' + [2] 'leaf1' + [3] 'leaf2' + [4] '' + [5] 'branch2' + [6] 'sub-branch1' + [7] 'leaf3' + [8] 'leaf4' + [9] '' + [10] '' + [11] 'sub-branch2' + [12] 'leaf5' + [13] '' + [14] 'branch3' + [-] undefined + [15] 'dual-indented' + [16] '' + [17] '' + +*/ \ No newline at end of file diff --git a/experiments/textnodes.mjs b/experiments/textnodes.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/package-manifest.yaml b/package-manifest.yaml index eaf9563..e9112f4 100644 --- a/package-manifest.yaml +++ b/package-manifest.yaml @@ -1,6 +1,6 @@ scope: '@efforting.tech' registry: 'https://npm.efforting.tech/' -version: 0.2.6 +version: 0.2.7 author: name: 'Mikael Lövqvist' @@ -23,11 +23,18 @@ packages: data: path: source/data - #documentation: documentation/data + documentation: documentation/data description: Data management internal-dependencies: - errors + text: + path: source/text + #documentation: documentation/text + description: Text management + internal-dependencies: + - data + wip-packages: object-graph-storage: path: source/object-graph-storage diff --git a/source/data/field-configuration-factories.mjs b/source/data/field-configuration-factories.mjs index d6f0749..c2b5d3b 100644 --- a/source/data/field-configuration-factories.mjs +++ b/source/data/field-configuration-factories.mjs @@ -26,3 +26,33 @@ export function required(description) { export function typed_required(coercion_function, description) { return new Field_Configuration((value) => value !== undefined, coercion_function, null, description); } + +export function symbol_set(description_to_name_mapping, description=null) { + + const symbols_by_name = Object.fromEntries(Object.keys(description_to_name_mapping).map(k => [k, Symbol(k)])); + const valid_symbols = new Set(Object.values(symbols_by_name)); + + const descriptions_by_symbol = Object.fromEntries(Object.entries(symbols_by_name).map(([n, s]) => [s, description_to_name_mapping[n]])); + + const result = new Field_Configuration( + (v) => valid_symbols.has(v), + (v) => typeof v === 'string' ? symbols_by_name[v] : v, // TODO: Assert that we could look up the symbol + null, + description, + ); + + // HACK: We are just tacking these on here but the proper method would be to create a proper subclass for the symbol set field type which is planned. + result.symbols = symbols_by_name; + result.symbol_descriptions = descriptions_by_symbol; + + return result; + +} + +export function cardinal_value(default_value=null, description=null) { + return new Field_Configuration((v) => Number.isInteger(v) && v >= 1, parseInt, () => default_value, description); +} + +export function natural_value(default_value=null, description=null) { + return new Field_Configuration((v) => Number.isInteger(v) && v >= 0, parseInt, () => default_value, description); +} \ No newline at end of file diff --git a/source/text/basic-tree.mjs b/source/text/basic-tree.mjs new file mode 100644 index 0000000..9fe932e --- /dev/null +++ b/source/text/basic-tree.mjs @@ -0,0 +1,16 @@ +import * as CF from '@efforting.tech/data/field-configuration-factories'; + + +export const Indention_Mode = new CF.symbol_set({ + AUTO: 'Automatic detection of indention mode', + SPACES: 'Indention is based on spaces', + TABULATORS: 'Indention is based on tabulators', +}, 'Indention mode'); + + +// BUG: Current implementation of CF.symbol_set doesn't support default value +export const Text_Settings = new CF.Schema({ + indention_mode: Indention_Mode, + indention_tabulator_width: CF.cardinal_value(4, 'Width of a tabulator in spaces'), + first_line: CF.natural_value(1, 'First line number'), +}, 'Text settings');