From 28de2a8e2091abac30e86ba7c0fb89d4a51e98e6 Mon Sep 17 00:00:00 2001 From: Joshua Bemenderfer Date: Sat, 12 Nov 2022 21:30:22 -0500 Subject: [PATCH] Working on API surface. --- packages/js/core/dist/document.cjs | 3 +- packages/js/core/dist/document.js | 50 +++-- packages/js/core/dist/index.cjs | 2 +- packages/js/core/dist/index.js | 10 +- packages/js/core/dist/parser.cjs | 2 +- packages/js/core/dist/parser.js | 30 +-- packages/js/core/dist/readers/js-string.cjs | 2 + packages/js/core/dist/readers/js-string.js | 12 ++ .../js/core/dist/readers/node-readline.cjs | 1 + .../js/core/dist/readers/node-readline.js | 10 + packages/js/core/package.json | 22 ++- packages/js/core/src/document.ts | 90 +++++++-- packages/js/core/src/parser.test.ts | 186 +++++++++--------- packages/js/core/src/parser.ts | 22 ++- packages/js/core/src/readers/js-string.ts | 17 ++ packages/js/core/src/readers/node-readline.ts | 10 + packages/js/core/src/readers/reader.ts | 5 + packages/js/core/vite.config.js | 13 +- 18 files changed, 325 insertions(+), 162 deletions(-) create mode 100644 packages/js/core/dist/readers/js-string.cjs create mode 100644 packages/js/core/dist/readers/js-string.js create mode 100644 packages/js/core/dist/readers/node-readline.cjs create mode 100644 packages/js/core/dist/readers/node-readline.js create mode 100644 packages/js/core/src/readers/js-string.ts create mode 100644 packages/js/core/src/readers/node-readline.ts create mode 100644 packages/js/core/src/readers/reader.ts diff --git a/packages/js/core/dist/document.cjs b/packages/js/core/dist/document.cjs index c908b81..4ea5743 100644 --- a/packages/js/core/dist/document.cjs +++ b/packages/js/core/dist/document.cjs @@ -1 +1,2 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const l=require("./parser.cjs");function s(a,i=" "){let e=null,t=l.LineData(),n=!1;async function r(){return e=await a(),e===null?n=!0:l.parseLine(e,t,i),{line:e,lineData:t,ended:n,next:r,current:u}}function u(){return{line:e,lineData:t,ended:n,next:r,current:u}}return{next:r,current:u}}exports.document=s; +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const s=require("./parser.cjs");function a(i,u=" "){let r=s.createLineData(null,u);const t={ended:!1,clone(){return a(i.clone(),u)},async next(){if(r.line=await i.next(),r.line===null)return!0;s.parseLine(r)},current(){return t},line(){var e;return(e=r.line)==null?void 0:e.slice(r.offsetHead)},head(){var e;return(e=r.line)==null?void 0:e.slice(r.offsetHead,r.offsetTail)},tail(){var e;return(e=r.line)==null?void 0:e.slice(r.offsetTail)},async content(e=-1,n=[]){var l;return e===-1&&(e=r.level+1),await t.next()||r.level1)throw new Error("'indent' must be a single-character string");let o=0,t=0;if(!r.length)e.type===1&&(t+=1),e.type===0&&(t=e.level),e.type=o,e.level=t,e.offsetHead=0,e.offsetTail=0;else{for(o=1;r[t]===f&&t<=e.level+1;)++t;for(e.type=o,e.level=t,e.offsetHead=t,e.offsetTail=t;r[e.offsetTail]&&r[e.offsetTail]!==" ";)++e.offsetTail}return e}exports.LineData=s;exports.parseLine=p; +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});function f(e=null,r=" "){return{line:e,indent:r,type:0,level:0,offsetHead:0,offsetTail:0}}function o(e){if(typeof e!="object"||!e||typeof e.type!="number"||typeof e.level!="number")throw new Error("'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties");if(typeof e.indent!="string"||e.indent.length===0||e.indent.length>1)throw new Error("'lineData.indent' must be a single-character string");if(typeof e.line!="string")throw new Error("'lineData.line' must be a string");let r=0,t=0;if(!e.line.length)e.type===1&&(t+=1),e.type===0&&(t=e.level),e.type=r,e.level=t,e.offsetHead=0,e.offsetTail=0;else{for(r=1;e.line[t]===e.indent&&t<=e.level+1;)++t;for(e.type=r,e.level=t,e.offsetHead=t,e.offsetTail=t;e.line[e.offsetTail]&&e.line[e.offsetTail]!==" ";)++e.offsetTail}return e}exports.createLineData=f;exports.parseLine=o; diff --git a/packages/js/core/dist/parser.js b/packages/js/core/dist/parser.js index 862a952..4e8c666 100644 --- a/packages/js/core/dist/parser.js +++ b/packages/js/core/dist/parser.js @@ -1,25 +1,25 @@ -function s() { - return { type: 0, level: 0, offsetHead: 0, offsetTail: 0 }; +function t(e = null, r = " ") { + return { line: e, indent: r, type: 0, level: 0, offsetHead: 0, offsetTail: 0 }; } -function p(r, e, t = " ") { - if (typeof r != "string") - throw new Error("'line' must be a string"); +function o(e) { if (typeof e != "object" || !e || typeof e.type != "number" || typeof e.level != "number") - throw new Error("'lineData' must be an object with 'type' and 'level' integer properties"); - if (typeof t != "string" || t.length === 0 || t.length > 1) - throw new Error("'indent' must be a single-character string"); - let o = 0, f = 0; - if (!r.length) - e.type === 1 && (f += 1), e.type === 0 && (f = e.level), e.type = o, e.level = f, e.offsetHead = 0, e.offsetTail = 0; + throw new Error("'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties"); + if (typeof e.indent != "string" || e.indent.length === 0 || e.indent.length > 1) + throw new Error("'lineData.indent' must be a single-character string"); + if (typeof e.line != "string") + throw new Error("'lineData.line' must be a string"); + let r = 0, f = 0; + if (!e.line.length) + e.type === 1 && (f += 1), e.type === 0 && (f = e.level), e.type = r, e.level = f, e.offsetHead = 0, e.offsetTail = 0; else { - for (o = 1; r[f] === t && f <= e.level + 1; ) + for (r = 1; e.line[f] === e.indent && f <= e.level + 1; ) ++f; - for (e.type = o, e.level = f, e.offsetHead = f, e.offsetTail = f; r[e.offsetTail] && r[e.offsetTail] !== " "; ) + for (e.type = r, e.level = f, e.offsetHead = f, e.offsetTail = f; e.line[e.offsetTail] && e.line[e.offsetTail] !== " "; ) ++e.offsetTail; } return e; } export { - s as LineData, - p as parseLine + t as createLineData, + o as parseLine }; diff --git a/packages/js/core/dist/readers/js-string.cjs b/packages/js/core/dist/readers/js-string.cjs new file mode 100644 index 0000000..2b258c7 --- /dev/null +++ b/packages/js/core/dist/readers/js-string.cjs @@ -0,0 +1,2 @@ +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});function i(r,n=0){const t=Array.isArray(r)?r:r.split(` +`),e={index:n-1,next:()=>(e.index++,e.index>=t.length?null:t[e.index]),clone:l=>i(r,l==null?n:e.index)};return e}exports.createStringReader=i; diff --git a/packages/js/core/dist/readers/js-string.js b/packages/js/core/dist/readers/js-string.js new file mode 100644 index 0000000..153f5f8 --- /dev/null +++ b/packages/js/core/dist/readers/js-string.js @@ -0,0 +1,12 @@ +function l(e, r = 0) { + const t = Array.isArray(e) ? e : e.split(` +`), n = { + index: r - 1, + next: () => (n.index++, n.index >= t.length ? null : t[n.index]), + clone: (i) => l(e, i == null ? r : n.index) + }; + return n; +} +export { + l as createStringReader +}; diff --git a/packages/js/core/dist/readers/node-readline.cjs b/packages/js/core/dist/readers/node-readline.cjs new file mode 100644 index 0000000..34b72cc --- /dev/null +++ b/packages/js/core/dist/readers/node-readline.cjs @@ -0,0 +1 @@ +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e={};function r(t){const a=e.createInterface({input:e.createReadStream(t,"utf-8")})[Symbol.asyncIterator]();return async()=>(await a.next()).value}exports.createReadlineReader=r; diff --git a/packages/js/core/dist/readers/node-readline.js b/packages/js/core/dist/readers/node-readline.js new file mode 100644 index 0000000..41951e7 --- /dev/null +++ b/packages/js/core/dist/readers/node-readline.js @@ -0,0 +1,10 @@ +const e = {}; +function n(t) { + const a = e.createInterface({ + input: e.createReadStream(t, "utf-8") + })[Symbol.asyncIterator](); + return async () => (await a.next()).value; +} +export { + n as createReadlineReader +}; diff --git a/packages/js/core/package.json b/packages/js/core/package.json index 320d088..9a3d976 100644 --- a/packages/js/core/package.json +++ b/packages/js/core/package.json @@ -4,8 +4,26 @@ "license": "MIT", "type": "module", "exports": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./parser": { + "import": "./dist/parser.js", + "require": "./dist/parser.cjs" + }, + "./document": { + "import": "./dist/document.js", + "require": "./dist/document.cjs" + }, + "./readers/node-readline": { + "import": "./dist/readers/node-readline.js", + "require": "./dist/readers/node-readline.cjs" + }, + "./readers/js-string": { + "import": "./dist/readers/js-string.js", + "require": "./dist/readers/js-string.cjs" + } }, "scripts": { "test": "vitest ./src", diff --git a/packages/js/core/src/document.ts b/packages/js/core/src/document.ts index 814ff02..51c88e1 100644 --- a/packages/js/core/src/document.ts +++ b/packages/js/core/src/document.ts @@ -1,20 +1,74 @@ -import { LineData, parseLine } from './parser' +import type { Reader } from './readers/reader' +import { createLineData, parseLine } from './parser' -export function document (nextFn: () => Promise, indent: string = ' ') { - let line: string|null = null - let lineData = LineData() - let ended = false - - async function next() { - line = await nextFn() - if (line === null) ended = true - else parseLine(line, lineData, indent) - return { line, lineData, ended, next, current } - } - - function current() { - return { line, lineData, ended, next, current } - } - - return { next, current } +type Document = { + ended: boolean, + clone: () => Document, + next: () => Promise + current: () => Document + line: () => string, + head: () => string, + tail: () => string, + content: (contentLevel: number, lines: string[]) => Promise, + seek: (matchHead: string, contentLevel: number) => Promise +} + +export function useDocument (reader: Reader, indent: string = ' '): Document { + let lineData = createLineData(null, indent) + + const document = { + ended: false, + + clone() { + return useDocument(reader.clone(), indent) + }, + + async next() { + lineData.line = await reader.next() + if (lineData.line === null) return true + else parseLine(lineData) + }, + + current() { + return document + }, + + line() { + return lineData.line?.slice(lineData.offsetHead) + }, + + head () { + return lineData.line?.slice(lineData.offsetHead, lineData.offsetTail) + }, + + tail () { + return lineData.line?.slice(lineData.offsetTail) + }, + + async content (contentLevel = -1, lines: string[] = []): Promise { + if (contentLevel === -1) contentLevel = lineData.level + 1 + + const ended = await document.next() + if (ended) return lines.join('\n') + + if (lineData.level < contentLevel) return lines.join('\n') + + lines.push(lineData.line?.slice(contentLevel) || '') + return document.content(contentLevel, lines) + }, + + async seek (matchHead: string, contentLevel = -1): Promise { + if (contentLevel === -1) contentLevel = lineData.level + + const ended = await document.next() + if (ended) return false + + if (document.head() === matchHead) return document + if (lineData.level < contentLevel) return false + + return document.seek(matchHead, contentLevel) + } + } + + return document } diff --git a/packages/js/core/src/parser.test.ts b/packages/js/core/src/parser.test.ts index 3562888..a29f495 100644 --- a/packages/js/core/src/parser.test.ts +++ b/packages/js/core/src/parser.test.ts @@ -1,145 +1,143 @@ import { describe, expect, it } from 'vitest' -import { LineData, parseLine } from './parser' +import { createLineData, parseLine } from './parser' describe(`LineData`, () => { it(`is an object`, () => { - const lineData = LineData() + const lineData = createLineData() expect(lineData).toBeTypeOf(`object`) }) - it(`has four properties`, () => { - const lineData = LineData() - expect(Object.keys(lineData).length).to.equal(4) + it(`has five properties`, () => { + const lineData = createLineData() + expect(Object.keys(lineData).length).to.equal(6) + }) + + it(`'line' is a string|null initialized to null`, () => { + const lineData = createLineData() + expect(lineData.level).to.equal(0) }) it(`'type' is an integer initialized to zero`, () => { - const lineData = LineData() + const lineData = createLineData() expect(lineData.level).to.equal(0) }) it(`'level' is an integer initialized to zero`, () => { - const lineData = LineData() + const lineData = createLineData() expect(lineData.type).to.equal(0) }) it(`'offsetHead' is an integer initialized to zero`, () => { - const lineData = LineData() + const lineData = createLineData() expect(lineData.offsetHead).to.equal(0) }) it(`'offsetTail' is an integer initialized to zero`, () => { - const lineData = LineData() + const lineData = createLineData() expect(lineData.offsetTail).to.equal(0) }) }) describe(`parseLine`, () => { - it(`Requres 'line' to be a string`, () => { - const lineData = LineData() - expect(() => parseLine(0, lineData)).toThrowError(`'line' must be a string`) + it(`Requres 'lineData' to be an object with a string line property and numeric level and type properties`, () => { + const lineData = createLineData() // @ts-ignore - expect(() => parseLine([], lineData)).toThrowError(`'line' must be a string`) + expect(() => parseLine(``, 0)).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine({}, lineData)).toThrowError(`'line' must be a string`) + expect(() => parseLine(``, [])).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(null, lineData)).toThrowError(`'line' must be a string`) + expect(() => parseLine(``, {})).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(true, lineData)).toThrowError(`'line' must be a string`) + expect(() => parseLine(``, null)).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(() => {}, lineData)).toThrowError(`'line' must be a string`) - }) - - it(`Requres 'lineData' to be an object with numeric level and type properties`, () => { - const lineData = LineData() + expect(() => parseLine(``, true)).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(``, 0)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) + expect(() => parseLine(``, () => {})).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(``, [])).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) + expect(() => parseLine(``, { line: '', level: '', type: 0 })).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) // @ts-ignore - expect(() => parseLine(``, {})).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) - // @ts-ignore - expect(() => parseLine(``, null)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) - // @ts-ignore - expect(() => parseLine(``, true)).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) - // @ts-ignore - expect(() => parseLine(``, () => {})).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) - // @ts-ignore - expect(() => parseLine(``, { level: '', type: 0 })).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) - // @ts-ignore - expect(() => parseLine(``, { level: 0, type: null })).toThrowError(`'lineData' must be an object with 'type' and 'level' integer properties`) + expect(() => parseLine(``, { line: '', level: 0, type: null })).toThrowError(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) }) it(`Requres 'indent' to be a single-character string`, () => { - const lineData = LineData() + const lineData = createLineData() + lineData.line = `` // @ts-ignore - expect(() => parseLine(``, lineData, 0)).toThrowError(`'indent' must be a single-character string`) + lineData.indent = 0 + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) // @ts-ignore - expect(() => parseLine(``, lineData, [])).toThrowError(`'indent' must be a single-character string`) + lineData.indent = [] + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) // @ts-ignore - expect(() => parseLine(``, lineData, {})).toThrowError(`'indent' must be a single-character string`) + lineData.indent = {} + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) // @ts-ignore - expect(() => parseLine(``, lineData, null)).toThrowError(`'indent' must be a single-character string`) + lineData.indent = null + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) // @ts-ignore - expect(() => parseLine(``, lineData, true)).toThrowError(`'indent' must be a single-character string`) + lineData.indent = true + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) // @ts-ignore - expect(() => parseLine(``, lineData, () => {})).toThrowError(`'indent' must be a single-character string`) - expect(() => parseLine(``, lineData, ` `)).toThrowError(`'indent' must be a single-character string`) + lineData.indent = () => {} + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) + lineData.indent = ` ` + expect(() => parseLine(lineData)).toThrowError(`'lineData.indent' must be a single-character string`) }) it(`Handles a blank line at indent level 0`, () => { const line = `` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 0, level: 0, offsetHead: 0, offsetTail: 0 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 0, level: 0, offsetHead: 0, offsetTail: 0 }) }) it(`Handles a line with a single space at indent level 1`, () => { const line = ` ` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 1, level: 1, offsetHead: 1, offsetTail: 1 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 1, level: 1, offsetHead: 1, offsetTail: 1 }) }) it(`Handles a line with two spaces`, () => { const line = ` ` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 1, level: 2, offsetHead: 2, offsetTail: 2 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 1, level: 2, offsetHead: 2, offsetTail: 2 }) }) it(`Handles a normal line at indent level 0`, () => { const line = `line 1` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 1, level: 0, offsetHead: 0, offsetTail: 4 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 1, level: 0, offsetHead: 0, offsetTail: 4 }) }) it(`Handles a normal line at indent level 1`, () => { const line = ` line 1` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 1, level: 1, offsetHead: 1, offsetTail: 5 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 1, level: 1, offsetHead: 1, offsetTail: 5 }) }) it(`Handles a normal line at indent level 2`, () => { const line = ` line 1` - const lineData = LineData() - parseLine(line, lineData) - expect(lineData).to.deep.equal({ type: 1, level: 2, offsetHead: 2, offsetTail: 6 }) + const lineData = createLineData(line) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: ` `, type: 1, level: 2, offsetHead: 2, offsetTail: 6 }) }) it(`Handles a normal line at indent level 1 indented with tabs`, () => { const line = `\tline 1` - const lineData = LineData() - parseLine(line, lineData, `\t`) - expect(lineData).to.deep.equal({ type: 1, level: 1, offsetHead: 1, offsetTail: 5 }) + const lineData = createLineData(line, `\t`) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: `\t`, type: 1, level: 1, offsetHead: 1, offsetTail: 5 }) }) it(`Handles a normal line at indent level 2 indented with tabs`, () => { const line = `\t\tline 1` - const lineData = LineData() - parseLine(line, lineData, `\t`) - expect(lineData).to.deep.equal({ type: 1, level: 2, offsetHead: 2, offsetTail: 6}) + const lineData = createLineData(line, `\t`) + parseLine(lineData) + expect(lineData).to.deep.equal({ line, indent: `\t`, type: 1, level: 2, offsetHead: 2, offsetTail: 6}) }) it(`Nests a normal line under a preceding normal line`, () => { @@ -148,15 +146,16 @@ describe(`parseLine`, () => { ' line 2' ] - const lineData = LineData() + const lineData = createLineData() const results = lines.map(line => { - parseLine(line, lineData) + lineData.line = line + parseLine(lineData) return {...lineData} }) expect(results).to.deep.equal([ - { type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, - { type: 1, level: 1, offsetHead: 1, offsetTail: 5 } + { line: lines[0], indent: ' ', type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, + { line: lines[1], indent: ' ', type: 1, level: 1, offsetHead: 1, offsetTail: 5 } ]) }) @@ -168,17 +167,18 @@ describe(`parseLine`, () => { ' line 4', ] - const lineData = LineData() + const lineData = createLineData() const results = lines.map(line => { - parseLine(line, lineData) + lineData.line = line + parseLine(lineData) return {...lineData} }) expect(results).to.deep.equal([ - { type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, - { type: 1, level: 1, offsetHead: 1, offsetTail: 5 }, - { type: 1, level: 1, offsetHead: 1, offsetTail: 5 }, - { type: 1, level: 1, offsetHead: 1, offsetTail: 5 } + { line: lines[0], indent: ' ', type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, + { line: lines[1], indent: ' ', type: 1, level: 1, offsetHead: 1, offsetTail: 5 }, + { line: lines[2], indent: ' ', type: 1, level: 1, offsetHead: 1, offsetTail: 5 }, + { line: lines[3], indent: ' ', type: 1, level: 1, offsetHead: 1, offsetTail: 5 } ]) }) @@ -188,15 +188,16 @@ describe(`parseLine`, () => { '' ] - const lineData = LineData() + const lineData = createLineData() const results = lines.map(line => { - parseLine(line, lineData) + lineData.line = line + parseLine(lineData) return {...lineData} }) expect(results).to.deep.equal([ - { type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, - { type: 0, level: 1, offsetHead: 0, offsetTail: 0 } + { line: lines[0], indent: ' ', type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, + { line: lines[1], indent: ' ', type: 0, level: 1, offsetHead: 0, offsetTail: 0 } ]) }) @@ -208,24 +209,25 @@ describe(`parseLine`, () => { '', ] - const lineData = LineData() + const lineData = createLineData() const results = lines.map(line => { - parseLine(line, lineData) + lineData.line = line + parseLine(lineData) return {...lineData} }) expect(results).to.deep.equal([ - { type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, - { type: 0, level: 1, offsetHead: 0, offsetTail: 0 }, - { type: 0, level: 1, offsetHead: 0, offsetTail: 0 }, - { type: 0, level: 1, offsetHead: 0, offsetTail: 0 } + { line: lines[0], indent: ' ', type: 1, level: 0, offsetHead: 0, offsetTail: 4 }, + { line: lines[1], indent: ' ', type: 0, level: 1, offsetHead: 0, offsetTail: 0 }, + { line: lines[2], indent: ' ', type: 0, level: 1, offsetHead: 0, offsetTail: 0 }, + { line: lines[3], indent: ' ', type: 0, level: 1, offsetHead: 0, offsetTail: 0 } ]) }) it(`Handle head and tail matching for lines with head and tail`, () => { const line = ` head tail1 tail2 tail3` - const lineData = LineData() - parseLine(line, lineData) + const lineData = createLineData(line) + parseLine(lineData) const head = line.slice(lineData.offsetHead, lineData.offsetTail) const tail = line.slice(lineData.offsetTail + 1) @@ -235,8 +237,8 @@ describe(`parseLine`, () => { it(`Handle head and tail matching for lines with head but no tail`, () => { const line = ` head` - const lineData = LineData() - parseLine(line, lineData) + const lineData = createLineData(line) + parseLine(lineData) const head = line.slice(lineData.offsetHead, lineData.offsetTail) const tail = line.slice(lineData.offsetTail + 1) @@ -246,8 +248,8 @@ describe(`parseLine`, () => { it(`Handle head and tail matching for lines with head and trailing space`, () => { const line = ` head ` - const lineData = LineData() - parseLine(line, lineData) + const lineData = createLineData(line) + parseLine(lineData) const head = line.slice(lineData.offsetHead, lineData.offsetTail) const tail = line.slice(lineData.offsetTail + 1) diff --git a/packages/js/core/src/parser.ts b/packages/js/core/src/parser.ts index c96d6df..0522255 100644 --- a/packages/js/core/src/parser.ts +++ b/packages/js/core/src/parser.ts @@ -1,23 +1,25 @@ -type LineData = { +export type LineData = { + line: string|null; + indent: string; type: number; level: number; offsetHead: number; offsetTail: number; } -export function LineData(): LineData { - return { type: 0, level: 0, offsetHead: 0, offsetTail: 0 } +export function createLineData(line: string|null = null, indent: string = ' '): LineData { + return { line, indent, type: 0, level: 0, offsetHead: 0, offsetTail: 0 } } -export function parseLine(line: string, lineData: LineData, indent: string = ' '): LineData { - if (typeof line !== 'string') throw new Error(`'line' must be a string`) - if ((typeof lineData !== 'object' || !lineData) || typeof lineData.type !== 'number' || typeof lineData.level !== 'number') throw new Error(`'lineData' must be an object with 'type' and 'level' integer properties`) - if (typeof indent !== 'string' || indent.length === 0 || indent.length > 1) throw new Error(`'indent' must be a single-character string`) +export function parseLine(lineData: LineData): LineData { + if ((typeof lineData !== 'object' || !lineData) || typeof lineData.type !== 'number' || typeof lineData.level !== 'number') throw new Error(`'lineData' must be an object with 'line' string, and 'type' and 'level' integer properties`) + if (typeof lineData.indent !== 'string' || lineData.indent.length === 0 || lineData.indent.length > 1) throw new Error(`'lineData.indent' must be a single-character string`) + if (typeof lineData.line !== 'string') throw new Error(`'lineData.line' must be a string`) let type = 0 let level = 0 - if (!line.length) { + if (!lineData.line.length) { if (lineData.type === 1) level += 1 if (lineData.type === 0) level = lineData.level @@ -28,13 +30,13 @@ export function parseLine(line: string, lineData: LineData, indent: string = ' ' } else { type = 1 - while (line[level] === indent && level <= lineData.level + 1) ++level + while (lineData.line[level] === lineData.indent && level <= lineData.level + 1) ++level lineData.type = type lineData.level = level lineData.offsetHead = level lineData.offsetTail = level - while (line[lineData.offsetTail] && line[lineData.offsetTail] !== ' ') ++lineData.offsetTail + while (lineData.line[lineData.offsetTail] && lineData.line[lineData.offsetTail] !== ' ') ++lineData.offsetTail } return lineData diff --git a/packages/js/core/src/readers/js-string.ts b/packages/js/core/src/readers/js-string.ts new file mode 100644 index 0000000..a3b7411 --- /dev/null +++ b/packages/js/core/src/readers/js-string.ts @@ -0,0 +1,17 @@ +import type { Reader } from './reader' + +export function createStringReader(doc: string|string[], index = 0): Reader { + const lines = Array.isArray(doc) ? doc : doc.split('\n') + + const reader = { + index: index - 1, + next: () => { + reader.index++ + if (reader.index >= lines.length) return null + return lines[reader.index] + }, + clone: (startIndex?: number) => createStringReader(doc, startIndex == null ? index : reader.index) + } + + return reader +} diff --git a/packages/js/core/src/readers/node-readline.ts b/packages/js/core/src/readers/node-readline.ts new file mode 100644 index 0000000..1d8ab48 --- /dev/null +++ b/packages/js/core/src/readers/node-readline.ts @@ -0,0 +1,10 @@ +import fs from 'node:fs' +import readline from 'node:readline/promises' + +export function createReadlineReader(path: string): () => Promise { + const it = readline.createInterface({ + input: fs.createReadStream(path, 'utf-8'), + })[Symbol.asyncIterator]() + + return async () => (await it.next()).value +} diff --git a/packages/js/core/src/readers/reader.ts b/packages/js/core/src/readers/reader.ts new file mode 100644 index 0000000..4d8add3 --- /dev/null +++ b/packages/js/core/src/readers/reader.ts @@ -0,0 +1,5 @@ +export type Reader = { + index: number, + next: () => string|null|Promise, + clone: (startIndex?: number) => Reader +} diff --git a/packages/js/core/vite.config.js b/packages/js/core/vite.config.js index 1380232..ecea8a2 100644 --- a/packages/js/core/vite.config.js +++ b/packages/js/core/vite.config.js @@ -1,16 +1,17 @@ // vite.config.js -import { resolve } from 'path' import { defineConfig } from 'vite' export default defineConfig({ build: { lib: { // Could also be a dictionary or array of multiple entry points - entry: [ - resolve(__dirname, 'src/index.ts'), - resolve(__dirname, 'src/document.ts'), - resolve(__dirname, 'src/parser.ts'), - ] + entry: { + 'index': 'src/index.ts', + 'document': 'src/document.ts', + 'parser': 'src/parser.ts', + 'readers/js-string': 'src/readers/js-string.ts', + 'readers/node-readline': 'src/readers/node-readline.ts', + } } } })