diff --git a/experiments/feature-stub.mjs b/experiments/feature-stub.mjs new file mode 100644 index 0000000..bc992ab --- /dev/null +++ b/experiments/feature-stub.mjs @@ -0,0 +1,30 @@ + +import { Stub } from '@efforting.tech/feature/stub'; +/* + +export function Stub(meta, name, description, module_name, function_name) { + return function stub() { + throw new Error(`The feature "${name}" of "${meta.url}" is not enabled. Enable it by calling "${function_name}(${this.name})" imported from "${module_name}"`); //TODO - specific error + } +} + +*/ + +class Thing { + + static from_stuff = Stub(import.meta, 'stuff-loader', 'Creates Thing from stuff', '@efforting.tech/stuff/loader', 'enable_stuff_loader'); + +} + +Thing.from_stuff() + +/* + +Error: The feature "stuff-loader" of "file:///srv/Projekt/efforting.tech/nodejs.esm-library/experiments/generic-parser-2.mjs" is not enabled. Enable it by calling "enable_stuff_loader(Thing)" imported from "@efforting.tech/stuff/loader" + at Thing.stub [as from_stuff] (file:///srv/Projekt/efforting.tech/nodejs.esm-library/build/packages/feature/stub.mjs:4:9) + at file:///srv/Projekt/efforting.tech/nodejs.esm-library/experiments/generic-parser-2.mjs:10:7 + at ModuleJob.run (node:internal/modules/esm/module_job:430:25) + at async node:internal/modules/esm/loader:639:26 + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) + +*/ \ No newline at end of file diff --git a/experiments/generic-parser-2.mjs b/experiments/generic-parser-2.mjs new file mode 100644 index 0000000..e69de29 diff --git a/experiments/table-1.mjs b/experiments/table-1.mjs new file mode 100644 index 0000000..c8747bd --- /dev/null +++ b/experiments/table-1.mjs @@ -0,0 +1,23 @@ +import { Row_Based_Table } from '@efforting.tech/table' + + +const t = new Row_Based_Table({ column_names: 'SKU, Quantity, Price' }); + +//console.log(t.column_names_lut); /* { SKU: 0, Quantity: 1, Price: 2 } */ + +t.push_rows( + ['VOLVO-1', 3, 120_000], + ['VOLVO-2', 4, 140_000], +) + +const [A] = t.read_rows(0); +const [B] = t.snapshot_rows(1); + +console.log(A.value); +console.log(B.value); +A.update({Quantity: 5}); +console.log(A.value); + +for (const r of t) { + console.log(r.object); +} diff --git a/experiments/table-2.mjs b/experiments/table-2.mjs new file mode 100644 index 0000000..f03255e --- /dev/null +++ b/experiments/table-2.mjs @@ -0,0 +1,34 @@ +import { Row_Based_Table } from '@efforting.tech/table'; +import { load_raster_table } from '@efforting.tech/table/raster-table'; + + +const t = load_raster_table(` + + SKU Quantity Price + --- -------- ----- + V-1 2 120_000 + V-2 3 140_000 + +`, Row_Based_Table); + + +// { row_name, column_name, row_index, column_index, row, cell } +t.replace_all_cells( + ({column_name, cell}) => { + const translation = { + SKU: (s) => s.trim(), + Quantity: parseInt, + Price: (p) => parseFloat(p.replace(/_/g, '')), + }[column_name]; + return translation ? translation(cell) : cell; + } +); + +for (const r of t) { + console.log(r.object); +} + +/* +{ SKU: 'V-1', Quantity: 2, Price: 120000 } +{ SKU: 'V-2', Quantity: 3, Price: 140000 } +*/ \ No newline at end of file diff --git a/package-manifest.yaml b/package-manifest.yaml index bae9e73..a271c87 100644 --- a/package-manifest.yaml +++ b/package-manifest.yaml @@ -1,3 +1,4 @@ +# TODO: Add a way to help making sure we keep internal-dependencies up to date scope: '@efforting.tech' registry: 'https://npm.efforting.tech/' version: 0.2.9 @@ -28,6 +29,17 @@ packages: internal-dependencies: - schema + table: + path: source/table + description: Table management + internal-dependencies: + - schema + - text + + feature: + path: source/feature + description: Feature management + schema: path: source/schema #documentation: documentation/schema diff --git a/source/feature/stub.mjs b/source/feature/stub.mjs new file mode 100644 index 0000000..afa2a75 --- /dev/null +++ b/source/feature/stub.mjs @@ -0,0 +1,7 @@ + +export function Stub(meta, name, description, module_name, function_name) { + return function stub() { + throw new Error(`The feature "${name}" of "${meta.url}" is not enabled. Enable it by calling "${function_name}(${this.name})" imported from "${module_name}"`); //TODO - specific error + } +} + diff --git a/source/table/raster-table.mjs b/source/table/raster-table.mjs new file mode 100644 index 0000000..df5f9fb --- /dev/null +++ b/source/table/raster-table.mjs @@ -0,0 +1,46 @@ +import { tabs_to_spaces } from '@efforting.tech/text/text-utilities'; + +const raster_table_pattern = /^((?:[^\S\n]|[\w-])+)\n((?:[^\S\n]|-)+)\n(.+)/ms; +const column_pattern = /\s*(.+?)(?=\s{2,}|$)/gd; +const row_pattern = /^(.+)$/mg + +// If loader is null we return a raw representation +export function load_raster_table(raster, loader=null, tab_width=4, null_padding=true) { + if (raster.match(/\t/g)) { + raster = tabs_to_spaces(raster, tab_width); + } + + if (null_padding) { //TODO - this is just experimental proof of concept + raster = raster.replace(/〃/g, '\0〃'); + } + + + const m = raster.match(raster_table_pattern); + const column_matches = [...m[1].matchAll(column_pattern)]; + const column_positions = [...column_matches.map(cm => cm.indices[1][0])] + const column_names = [...column_matches.map(cm => cm[1])] + const rows = []; + + for (const row of m[3].matchAll(row_pattern)) { + + const pending_row = []; + for (let ci=0; ci [], 'Rows to initialize table with'), + row_names: F.typed_factory((v) => typeof v === 'string' ? parse_csv(v) : v, () => [], 'Names of rows'), + column_names: F.typed_factory((v) => typeof v === 'string' ? parse_csv(v) : v, () => [], 'Names of columns'), + +}, 'Row based table settings'); + + +export class Table_Row_Reference { + + constructor(table, index, snapshot=null) { + Object.assign(this, { table, index, snapshot }); + } + + get value() { + const { table, index, snapshot } = this; + return snapshot ?? table.read_row(index); + } + + get object() { + const { table, index, snapshot } = this; + const { column_names } = table; + const value = snapshot ?? table.read_row(index); + //TODO - check shape of column_names vs the value + + return Object.fromEntries(value.map((cell, column_index) => [column_names[column_index], cell])); + } + + + update(updates) { + const { table, index, snapshot } = this; + if (snapshot) { + throw new Error('Can not update snapshot references, clear the snapshot to reuse this index'); //TODO - proper error + } + table.write_row(index, updates); + } + +} + +export class Row_Based_Table { + + constructor(settings) { + const { rows, column_names, row_names } = Row_Based_Table_Settings.load(settings); + Object.assign(this, { rows }); + this.set_column_names(...column_names); + this.set_row_names(...row_names); + } + + // General operations + + get width() { + const { rows, column_names } = this; + return rows[0]?.length || column_names.length || undefined; + } + + get length() { + const { rows } = this; + return rows.length; + } + + get size() { + const { width, length } = this; + return [width, length]; + } + + replace_all_cells(replacement_fn) { + + for (const [row_index, row] of this.rows.entries()) { + const pending_row = []; + for (const [column_index, cell] of row.entries()) { + const row_name = this.row_names[row_index]; + const column_name = this.column_names[column_index]; + pending_row.push(replacement_fn({ row_name, column_name, row_index, column_index, row, cell })); + } + this.rows[row_index] = pending_row; + } + + + } + + // TODO: Make sure column and row operations covers the same uses + // TODO: Sub table operations + + // Column operations + + set_column_names(...names) { + this.column_names = names; + this.column_names_lut = Object.fromEntries(names.map((name, index) => [name, index])); + } + + // Row operations + + set_row_names(...names) { + this.row_names = names; + this.row_names_lut = Object.fromEntries(names.map((name, index) => [name, index])); + } + + read_rows(...indices) { + const result = []; + for (const index of indices) { + result.push(new Table_Row_Reference(this, index)); + } + return result; + } + + snapshot_rows(...indices) { + //NOTE: Snapshot doesn't include current layout or other settings, just the contents of the row at the time, and it currently is a shallow copy + const result = []; + for (const index of indices) { + result.push(new Table_Row_Reference(this, index, [...this.rows[index]])); + } + return result; + } + + push_rows(...rows) { + //TODO - verify shape + this.rows.push(...rows); + } + + read_row(index) { + return this.rows[index]; + } + + write_row(index, row_data) { + const { rows, column_names_lut } = this; + if (Array.isArray(row_data)) { + //TODO - shape check + rows[index] = row_data; + } else { + //TODO - possibly allow array of array for using numerical indices + const work_row = rows[index]; + for (const [key, value] of Object.entries(row_data)) { + const col_index = column_names_lut[key]; + + if (col_index === undefined) { + throw new Error(`Unknown column: ${key}`); //TODO - proper error + } + + work_row[col_index] = value; + } + } + } + + + *[Symbol.iterator]() { + for (const index of this.rows.keys()) { + yield new Table_Row_Reference(this, index); + } + } + + +} + diff --git a/source/text/text-utilities.mjs b/source/text/text-utilities.mjs new file mode 100644 index 0000000..3283341 --- /dev/null +++ b/source/text/text-utilities.mjs @@ -0,0 +1,30 @@ +export const char_width_lut = { + '〃': 2, +} + + +//NOTE - this one only support posix newlines, this should be fixed later +export function tabs_to_spaces(text, tab_width=4) { + let pending_column_index=0; + + let result = ''; + for (const char of text) { + if (char == '\t') { + const new_column_index = (Math.floor(pending_column_index / tab_width) + 1) * tab_width; + result += ' '.repeat(new_column_index - pending_column_index); + pending_column_index = new_column_index; + } else { + if (char.charCodeAt(0) === 10) { + result += '\n'; + pending_column_index = 0; +/* } else if (char.charCodeAt(0) < 32) { + result += '�'; +*/ } else { + pending_column_index += (char_width_lut[char] ?? 1) + result += char; + } + } + } + return result; + +} diff --git a/tools/stage-for-npm.mjs b/tools/stage-for-npm.mjs index 7a04c37..320f107 100644 --- a/tools/stage-for-npm.mjs +++ b/tools/stage-for-npm.mjs @@ -88,6 +88,9 @@ for (const [package_name, package_data] of Object.entries(manifest.packages)) { const linked_docs = pkg.documentation ? link_tree(pkg.documentation, pkg_dir).map(p => path.relative(pkg_dir, p)) : []; //console.log('DOCS', { linked_docs }); + + // A file named after its package (e.g. table.mjs in the table package) becomes the root export '.' + // All other files become named subpath exports e.g. './raster-table' const exports_map = {}; for (const file of linked_sources) { const name = path.basename(file, '.mjs');