New implementation of v3, should be more foolproof with simpler schemas.
This commit is contained in:
@@ -1,76 +1,77 @@
|
||||
import { useDocument } from '@terrace/core'
|
||||
import { createStringReader } from '@terrace/core/readers/js-string'
|
||||
|
||||
export async function parse(lines, schema) {
|
||||
export async function parse(lines, rootSchema) {
|
||||
const doc = useDocument(createStringReader(lines))
|
||||
|
||||
function createScope(line, matchers) {
|
||||
return {
|
||||
block: [line, []],
|
||||
handlers: [],
|
||||
matchers: matchers
|
||||
const levelTracker = []
|
||||
|
||||
function createScope(level, line, schema = null) {
|
||||
levelTracker.length = level
|
||||
const entry = levelTracker[level] = {}
|
||||
entry.scope = [line, []]
|
||||
|
||||
if (schema) {
|
||||
entry.schemas = Object.values(schema),
|
||||
entry.matchers = Object.keys(schema),
|
||||
entry.counts = Object.values(schema).map(e => e.count)
|
||||
}
|
||||
|
||||
return entry.scope
|
||||
}
|
||||
|
||||
createScope(0, 'root', rootSchema)
|
||||
|
||||
// Simpler parsing logic, don't need to worry about schemas, matchers, or counts if no schema is specified.
|
||||
if (!rootSchema) {
|
||||
while (true) {
|
||||
// If doc.next() returns true we've ended the document.
|
||||
if (await doc.next()) break;
|
||||
const level = doc.level()
|
||||
// Determine parent for this scope.
|
||||
const parent = levelTracker[level].scope
|
||||
// If there's no parent, skip this line.
|
||||
if (!parent) continue
|
||||
|
||||
// Create new scope
|
||||
const scope = createScope(level + 1, doc.line())
|
||||
// Add current scope to parent.
|
||||
parent[1].push(scope)
|
||||
}
|
||||
// Full parsing logic
|
||||
} else {
|
||||
while (true) {
|
||||
// If doc.next() returns true we've ended the document.
|
||||
if (await doc.next()) break;
|
||||
const level = doc.level()
|
||||
if (!levelTracker[level]) continue
|
||||
|
||||
// Determine parent for this scope.
|
||||
const parent = levelTracker[level].scope
|
||||
const schemas = levelTracker[level].schemas
|
||||
const matchers = levelTracker[level].matchers
|
||||
const counts = levelTracker[level].counts
|
||||
|
||||
// Match the head value, or '?' for unspecified lines.
|
||||
const matchIndex = matchers.findIndex(entry => entry === doc.head() || entry === '?')
|
||||
|
||||
// Handle trailing blocks of text. TODO: Proper trailing.
|
||||
if (matchIndex === -1 && matchers.includes('? literal')) {
|
||||
parent[1].push(...(await doc.content(level)).map(e => [e]))
|
||||
continue
|
||||
} else if (matchIndex === -1) continue
|
||||
|
||||
// Return if the match has already been "used up"
|
||||
if (counts[matchIndex] === 0) continue
|
||||
// "use up" one more match
|
||||
counts[matchIndex] -= 1
|
||||
|
||||
const scopeSchema = schemas[matchIndex]
|
||||
// Create new scope
|
||||
const scope = createScope(level + 1, doc.line(), scopeSchema?.children)
|
||||
parent[1].push(scope)
|
||||
}
|
||||
}
|
||||
|
||||
const scopes = [createScope('root', {
|
||||
title ({ addHandler }) {
|
||||
addHandler(scope => [scope[0].split(' ')[0], scope[0].split(' ').slice(1).join(' ')])
|
||||
},
|
||||
options ({ addMatcher }) {
|
||||
addMatcher({
|
||||
parameter1({ addHandler }) {
|
||||
addHandler(scope => [scope[0].split(' ')[0], +scope[0].split(' ').slice(1).join(' ')])
|
||||
}
|
||||
})
|
||||
}
|
||||
})]
|
||||
|
||||
while (true) {
|
||||
// If doc.next() returns true we've ended the document.
|
||||
if (await doc.next()) break;
|
||||
const level = doc.level() + 1
|
||||
|
||||
// Trigger macros for closed scopes.
|
||||
for (let i = scopes.length - 1; i >= level; --i) {
|
||||
const scope = scopes[i]
|
||||
const parent = scopes[i - 1]
|
||||
|
||||
// Remove empty arrays from scopes without children.
|
||||
if (scope.block[1] && !scope.block[1].length) scope.block.length = 1
|
||||
|
||||
// Postprocess scope with relevant macros.
|
||||
scope.block = scope.handlers.reduce((block, handler) => {
|
||||
return handler(block)
|
||||
}, [...scope.block])
|
||||
|
||||
// Add to parent block.
|
||||
parent.block[1].push(scope.block)
|
||||
}
|
||||
|
||||
// Reset scope length to avoid dangling scopes.
|
||||
scopes.length = level
|
||||
|
||||
// Define current scope
|
||||
const scope = scopes[level] = createScope(doc.line(), {})
|
||||
// Determine parent for this scope.
|
||||
const parent = scopes[level - 1]
|
||||
|
||||
// Add a postprocess handler for this scope.
|
||||
// Ie. When we leave the scope, rewrite it using this handler.
|
||||
function addHandler(handler) {
|
||||
scope.handlers.push(handler)
|
||||
}
|
||||
|
||||
// Add matchers for this scope's children.
|
||||
function addMatcher(definition) {
|
||||
scope.matchers = definition
|
||||
}
|
||||
|
||||
// Run matching matchers for this scope.
|
||||
if (parent.matchers[doc.head()]) {
|
||||
parent.matchers[doc.head()]({ addHandler, addMatcher })
|
||||
}
|
||||
}
|
||||
|
||||
return scopes[0].block
|
||||
}
|
||||
return levelTracker[0].scope
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { parse } from './core.js'
|
||||
|
||||
const schema = {
|
||||
}
|
||||
|
||||
|
||||
const lines = [
|
||||
const linesFull = [
|
||||
`title Example`,
|
||||
`options`,
|
||||
` parameter1 30`,
|
||||
@@ -12,11 +8,13 @@ const lines = [
|
||||
`options`,
|
||||
` parameter1 0`,
|
||||
` parameter2 Esse incididunt et est adipisicing eiusmod aliqua enim ea aliqua id enim.`,
|
||||
` deep Enim fugiat do in est commodo culpa dolore.`,
|
||||
`subsection`,
|
||||
` position 1`,
|
||||
` Ea dolore in aliquip fugiat anim adipisicing amet aute tempor et deserunt est duis sint.`,
|
||||
`subsection 2`,
|
||||
` position 2`,
|
||||
` `,
|
||||
` Aute deserunt incididunt ad in sint adipisicing est officia velit pariatur ipsum deserunt quis nulla.`,
|
||||
` Ea dolore in aliquip fugiat anim adipisicing amet aute tempor et deserunt est duis sint.`,
|
||||
`list`,
|
||||
@@ -36,8 +34,75 @@ const lines = [
|
||||
` 2`
|
||||
]
|
||||
|
||||
const linesSchema = [
|
||||
`name @terrace/core`,
|
||||
`version 0.0.1`,
|
||||
`randomthing test`,
|
||||
`license MIT`,
|
||||
`license GPL`,
|
||||
`exports`,
|
||||
` .`,
|
||||
` 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`,
|
||||
` build vite build`,
|
||||
`devDependencies`,
|
||||
` vite ^3.2.3`,
|
||||
` vitest ^0.24.5`,
|
||||
``,
|
||||
`author`,
|
||||
` name Joshua Bemenderfer`,
|
||||
` email josh@thederf.com`,
|
||||
` `,
|
||||
` Further comments below. As I will now demonstrate, there is no simple`,
|
||||
` way of dealing with this problem.`,
|
||||
]
|
||||
|
||||
const schema = {
|
||||
"name": {count: 1},
|
||||
"version": {count: 1},
|
||||
"license": {count: 1},
|
||||
"exports": {count: 1, children: {
|
||||
"?": {count: -1, children: {
|
||||
"import": {count: 1},
|
||||
"require": {count: 1}
|
||||
}}
|
||||
}},
|
||||
"scripts": {count: 1, children: {
|
||||
"?": { count: -1 }
|
||||
}},
|
||||
"devDependencies": {count: 1, children: {
|
||||
"?": { count: -1 }
|
||||
}},
|
||||
"author": { count: 1, children: {
|
||||
"name": { count: 1 },
|
||||
"email": { count: 1 },
|
||||
"? literal": { count: -1 }
|
||||
}}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.dir(await parse(lines, schema), { depth: null })
|
||||
const resultFull = await parse(linesFull)
|
||||
// console.dir(resultFull, { depth: null })
|
||||
|
||||
const resultSchema = await parse(linesSchema, schema)
|
||||
console.dir(resultSchema, { depth: null })
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user