diff --git a/package-lock.json b/package-lock.json index 5ee7dcd..46a07e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@types/node": "^22.5.5", + "strip-ansi": "^7.1.0", "typescript": "^5.6.2" } }, @@ -70,6 +71,19 @@ "undici-types": "~6.19.2" } }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -79,6 +93,22 @@ "node": ">=6" } }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", diff --git a/package.json b/package.json index 2dfee27..4907613 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@types/node": "^22.5.5", + "strip-ansi": "^7.1.0", "typescript": "^5.6.2" }, "dependencies": { diff --git a/src/language/cli/output.ts b/src/language/cli/output.ts index 3c15219..7cafdfa 100644 --- a/src/language/cli/output.ts +++ b/src/language/cli/output.ts @@ -1,9 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import { parseArgs } from 'util' import { type SyntaxTree } from '../parsing/syntax-tree.js' -import { unparse, type Notation } from '../unparsing.js' -import { prettyJson } from '../unparsing/pretty-json.js' -import { prettyPlz } from '../unparsing/pretty-plz.js' +import { prettyJson, prettyPlz, unparse, type Notation } from '../unparsing.js' export const handleOutput = async ( process: NodeJS.Process, diff --git a/src/language/compiling/unparsing.test.ts b/src/language/compiling/unparsing.test.ts new file mode 100644 index 0000000..b5513e5 --- /dev/null +++ b/src/language/compiling/unparsing.test.ts @@ -0,0 +1,123 @@ +import either from '@matt.kantor/either' +import stripAnsi from 'strip-ansi' +import { testCases } from '../../test-utilities.test.js' +import { type Atom, type Molecule } from '../parsing.js' +import { + inlinePlz, + prettyJson, + prettyPlz, + type Notation, +} from '../unparsing.js' + +const unparser = (notation: Notation) => (value: Atom | Molecule) => + either.map( + typeof value === 'string' + ? notation.atom(value) + : notation.molecule(value, notation), + stripAnsi, // terminal styling is not currently tested + ) + +testCases( + unparser(inlinePlz), + input => `unparsing \`${JSON.stringify(input)}\``, +)('inline plz', [ + [{}, either.makeRight('{}')], + ['a', either.makeRight('a')], + ['Hello, world!', either.makeRight('"Hello, world!"')], + [{ 0: 'a' }, either.makeRight('{ a }')], + [{ 1: 'a' }, either.makeRight('{ 1: a }')], + [ + { 0: 'a', 1: 'b', 3: 'c', somethingElse: 'd' }, + either.makeRight('{ a, b, 3: c, somethingElse: d }'), + ], + [{ a: { b: { c: 'd' } } }, either.makeRight('{ a: { b: { c: d } } }')], + [ + { + identity: { + 0: '@function', + parameter: 'a', + body: { 0: '@lookup', 1: 'a' }, + }, + test: { + 0: '@apply', + function: { 0: '@lookup', 1: 'identity' }, + argument: 'it works!', + }, + }, + either.makeRight('{ identity: a => :a, test: :identity("it works!") }'), + ], +]) + +testCases( + unparser(prettyPlz), + input => `unparsing \`${JSON.stringify(input)}\``, +)('pretty plz', [ + [{}, either.makeRight('{}')], + ['a', either.makeRight('a')], + ['Hello, world!', either.makeRight('"Hello, world!"')], + [{ 0: 'a' }, either.makeRight('{\n a\n}')], + [{ 1: 'a' }, either.makeRight('{\n 1: a\n}')], + [ + { 0: 'a', 1: 'b', 3: 'c', somethingElse: 'd' }, + either.makeRight('{\n a\n b\n 3: c\n somethingElse: d\n}'), + ], + [ + { a: { b: { c: 'd' } } }, + either.makeRight('{\n a: {\n b: {\n c: d\n }\n }\n}'), + ], + [ + { + identity: { + 0: '@function', + parameter: 'a', + body: { 0: '@lookup', 1: 'a' }, + }, + test: { + 0: '@apply', + function: { 0: '@lookup', 1: 'identity' }, + argument: 'it works!', + }, + }, + either.makeRight( + '{\n identity: a => :a\n test: :identity("it works!")\n}', + ), + ], +]) + +testCases( + unparser(prettyJson), + input => `unparsing \`${JSON.stringify(input)}\``, +)('pretty JSON', [ + [{}, either.makeRight('{}')], + ['a', either.makeRight('"a"')], + ['Hello, world!', either.makeRight('"Hello, world!"')], + [{ 0: 'a' }, either.makeRight('{\n "0": "a"\n}')], + [{ 1: 'a' }, either.makeRight('{\n "1": "a"\n}')], + [ + { 0: 'a', 1: 'b', 3: 'c', somethingElse: 'd' }, + either.makeRight( + '{\n "0": "a",\n "1": "b",\n "3": "c",\n "somethingElse": "d"\n}', + ), + ], + [ + { a: { b: { c: 'd' } } }, + either.makeRight('{\n "a": {\n "b": {\n "c": "d"\n }\n }\n}'), + ], + [ + { + identity: { + 0: '@function', + parameter: 'a', + body: { 0: '@lookup', 1: 'a' }, + }, + test: { + 0: '@apply', + function: { 0: '@lookup', 1: 'identity' }, + argument: 'it works!', + }, + }, + either.makeRight( + '{\n "identity": {\n "0": "@function",\n "parameter": "a",\n "body": {\n "0": "@lookup",\n "1": "a"\n }\n },\n "test": {\n "0": "@apply",\n "function": {\n "0": "@lookup",\n "1": "identity"\n },\n "argument": "it works!"\n }\n}', + ), + ], +]) diff --git a/src/language/runtime/keywords.ts b/src/language/runtime/keywords.ts index 27a8110..8e44301 100644 --- a/src/language/runtime/keywords.ts +++ b/src/language/runtime/keywords.ts @@ -18,7 +18,7 @@ import { lookupPropertyOfObjectNode, makeUnelaboratedObjectNode, } from '../semantics/object-node.js' -import { prettyJson } from '../unparsing/pretty-json.js' +import { prettyJson } from '../unparsing.js' const unserializableFunction = () => either.makeLeft({ diff --git a/src/language/semantics/key-path.ts b/src/language/semantics/key-path.ts index 6c2743c..a8e04e2 100644 --- a/src/language/semantics/key-path.ts +++ b/src/language/semantics/key-path.ts @@ -1,7 +1,6 @@ import either from '@matt.kantor/either' import type { Atom, Molecule } from '../parsing.js' -import { unparse } from '../unparsing.js' -import { inlinePlz } from '../unparsing/inline-plz.js' +import { inlinePlz, unparse } from '../unparsing.js' export type KeyPath = readonly Atom[] diff --git a/src/language/semantics/semantic-graph.ts b/src/language/semantics/semantic-graph.ts index 3787bb8..078c36d 100644 --- a/src/language/semantics/semantic-graph.ts +++ b/src/language/semantics/semantic-graph.ts @@ -7,8 +7,7 @@ import type { } from '../errors.js' import type { Atom, Molecule } from '../parsing.js' import type { Canonicalized } from '../parsing/syntax-tree.js' -import { unparse } from '../unparsing.js' -import { inlinePlz } from '../unparsing/inline-plz.js' +import { inlinePlz, unparse } from '../unparsing.js' import { serializeFunctionNode, type FunctionNode } from './function-node.js' import { stringifyKeyPathForEndUser, type KeyPath } from './key-path.js' import { diff --git a/src/language/unparsing.ts b/src/language/unparsing.ts index 0414312..989cd53 100644 --- a/src/language/unparsing.ts +++ b/src/language/unparsing.ts @@ -3,7 +3,10 @@ import type { UnserializableValueError } from './errors.js' import type { Atom, Molecule } from './parsing.js' import type { Notation } from './unparsing/unparsing-utilities.js' -export { type Notation } from './unparsing/unparsing-utilities.js' +export { inlinePlz } from './unparsing/inline-plz.js' +export { prettyJson } from './unparsing/pretty-json.js' +export { prettyPlz } from './unparsing/pretty-plz.js' +export type { Notation } from './unparsing/unparsing-utilities.js' export const unparse = ( value: Atom | Molecule, diff --git a/src/language/unparsing/inline-plz.ts b/src/language/unparsing/inline-plz.ts index a427722..d210418 100644 --- a/src/language/unparsing/inline-plz.ts +++ b/src/language/unparsing/inline-plz.ts @@ -1,186 +1,40 @@ -import type { Right } from '@matt.kantor/either' -import either, { type Either } from '@matt.kantor/either' -import kleur from 'kleur' -import type { UnserializableValueError } from '../errors.js' +import either from '@matt.kantor/either' import type { Atom, Molecule } from '../parsing.js' -import { unquotedAtomParser } from '../parsing/atom.js' import { - isSemanticGraph, - readApplyExpression, - readFunctionExpression, - readLookupExpression, - serialize, - type KeyPath, - type SemanticGraph, -} from '../semantics.js' -import { type Notation } from './unparsing-utilities.js' - -// TODO: Share implementation details with pretty plz notation. - -const dot = kleur.dim('.') -const quote = kleur.dim('"') -const colon = kleur.dim(':') -const comma = kleur.dim(',') -const openBrace = kleur.dim('{') -const closeBrace = kleur.dim('}') -const openParenthesis = kleur.dim('(') -const closeParenthesis = kleur.dim(')') -const arrow = kleur.dim('=>') - -const escapeStringContents = (value: string) => - value.replace('\\', '\\\\').replace('"', '\\"') - -const quoteIfNecessary = (value: string) => { - const unquotedAtomResult = unquotedAtomParser(value) - if ( - either.isLeft(unquotedAtomResult) || - unquotedAtomResult.value.remainingInput.length !== 0 - ) { - return quote.concat(escapeStringContents(value)).concat(quote) - } else { - return value - } -} - -const atom = (node: string): Right => - either.makeRight( - quoteIfNecessary( - /^@[^@]/.test(node) ? kleur.bold(kleur.underline(node)) : node, - ), - ) - -const molecule = ( - value: Molecule, -): Either => { - const functionExpressionResult = readFunctionExpression(value) - if (!either.isLeft(functionExpressionResult)) { - return sugaredFunction( - functionExpressionResult.value.parameter, - functionExpressionResult.value.body, - ) - } else { - const applyExpressionResult = readApplyExpression(value) - if (!either.isLeft(applyExpressionResult)) { - return sugaredApply( - applyExpressionResult.value.argument, - applyExpressionResult.value.function, - ) - } else { - const lookupExpressionResult = readLookupExpression(value) - if (!either.isLeft(lookupExpressionResult)) { - return sugaredLookup(lookupExpressionResult.value.query) - } else { - return sugarFreeMolecule(value) - } - } - } -} - -const sugaredLookup = (keyPathAsNode: Molecule | SemanticGraph) => { - const keyPath = Object.entries(keyPathAsNode).reduce( - (accumulator: KeyPath | 'invalid', [key, value]) => { - if (accumulator === 'invalid') { - return accumulator - } else { - if (key === String(accumulator.length) && typeof value === 'string') { - return [...accumulator, value] - } else { - return 'invalid' - } - } - }, - [], - ) - - if ( - keyPath !== 'invalid' && - Object.keys(keyPathAsNode).length === keyPath.length && - keyPath.every(key => !either.isLeft(unquotedAtomParser(key))) - ) { - return either.makeRight(kleur.cyan(colon.concat(keyPath.join(dot)))) - } else { - return either.flatMap(serializeIfNeeded(keyPathAsNode), serializedKeyPath => - either.map(atomOrMolecule(serializedKeyPath), keyPathAsString => - kleur.cyan(colon.concat(keyPathAsString)), - ), - ) - } -} - -const sugaredFunction = ( - parameterName: string, - body: Molecule | SemanticGraph, -) => - either.flatMap(serializeIfNeeded(body), serializedBody => - either.map(atomOrMolecule(serializedBody), bodyAsString => - [kleur.cyan(parameterName), arrow, bodyAsString].join(' '), - ), - ) - -const sugaredApply = ( - argument: Molecule | SemanticGraph, - functionToApply: Molecule | SemanticGraph, -) => - either.flatMap(serializeIfNeeded(functionToApply), serializedFunction => - either.flatMap( - atomOrMolecule(serializedFunction), - functionToApplyAsString => - either.flatMap(serializeIfNeeded(argument), serializedArgument => - either.map(atomOrMolecule(serializedArgument), argumentAsString => - functionToApplyAsString - .concat(openParenthesis) - .concat(argumentAsString) - .concat(closeParenthesis), - ), - ), - ), - ) - -const sugarFreeMolecule = (value: Molecule) => { - const entries = Object.entries(value) - if (entries.length === 0) { + closeBrace, + comma, + moleculeUnparser, + openBrace, + sugarFreeMoleculeAsKeyValuePairStrings, + unparseAtom, +} from './plz-utilities.js' +import type { Notation } from './unparsing-utilities.js' + +const unparseSugarFreeMolecule = (value: Molecule) => { + if (Object.keys(value).length === 0) { return either.makeRight(openBrace + closeBrace) } else { - const keyValuePairsAsStrings: string[] = [] - let ordinalPropertyKeyCounter = 0n - for (const [propertyKey, propertyValue] of entries) { - const valueAsStringResult = atomOrMolecule(propertyValue) - if (either.isLeft(valueAsStringResult)) { - return valueAsStringResult - } - - // Omit ordinal property keys: - if (propertyKey === String(ordinalPropertyKeyCounter)) { - keyValuePairsAsStrings.push(valueAsStringResult.value) - ordinalPropertyKeyCounter += 1n - } else { - keyValuePairsAsStrings.push( - kleur - .cyan(quoteIfNecessary(propertyKey).concat(colon)) - .concat(' ') - .concat(valueAsStringResult.value), - ) - } - } - - return either.makeRight( - openBrace - .concat(' ') - .concat(keyValuePairsAsStrings.join(comma.concat(' '))) - .concat(' ') - .concat(closeBrace), + return either.map( + sugarFreeMoleculeAsKeyValuePairStrings(value, unparseAtomOrMolecule), + keyValuePairsAsStrings => + openBrace + .concat(' ') + .concat(keyValuePairsAsStrings.join(comma.concat(' '))) + .concat(' ') + .concat(closeBrace), ) } } -const serializeIfNeeded = ( - nodeOrMolecule: SemanticGraph | Molecule, -): Either => - isSemanticGraph(nodeOrMolecule) - ? serialize(nodeOrMolecule) - : either.makeRight(nodeOrMolecule) +const unparseAtomOrMolecule = (value: Atom | Molecule) => + typeof value === 'string' ? unparseAtom(value) : unparseMolecule(value) -const atomOrMolecule = (value: Atom | Molecule) => - typeof value === 'string' ? atom(value) : molecule(value) +const unparseMolecule = moleculeUnparser( + unparseAtomOrMolecule, + unparseSugarFreeMolecule, +) -export const inlinePlz: Notation = { atom, molecule } +export const inlinePlz: Notation = { + atom: unparseAtom, + molecule: unparseMolecule, +} diff --git a/src/language/unparsing/plz-utilities.ts b/src/language/unparsing/plz-utilities.ts new file mode 100644 index 0000000..8c1038c --- /dev/null +++ b/src/language/unparsing/plz-utilities.ts @@ -0,0 +1,195 @@ +import either, { type Either, type Right } from '@matt.kantor/either' +import kleur from 'kleur' +import type { UnserializableValueError } from '../errors.js' +import type { Atom, Molecule } from '../parsing.js' +import { unquotedAtomParser } from '../parsing/atom.js' +import { + isSemanticGraph, + readApplyExpression, + readFunctionExpression, + readLookupExpression, + serialize, + type ApplyExpression, + type FunctionExpression, + type KeyPath, + type LookupExpression, + type SemanticGraph, +} from '../semantics.js' + +export const dot = kleur.dim('.') +export const quote = kleur.dim('"') +export const colon = kleur.dim(':') +export const comma = kleur.dim(',') +export const openBrace = kleur.dim('{') +export const closeBrace = kleur.dim('}') +export const openParenthesis = kleur.dim('(') +export const closeParenthesis = kleur.dim(')') +export const arrow = kleur.dim('=>') + +export const moleculeUnparser = + ( + unparseAtomOrMolecule: UnparseAtomOrMolecule, + unparseSugarFreeMolecule: ( + expression: Molecule, + unparseAtomOrMolecule: UnparseAtomOrMolecule, + ) => Either, + ) => + (value: Molecule): Either => { + const functionExpressionResult = readFunctionExpression(value) + if (!either.isLeft(functionExpressionResult)) { + return unparseSugaredFunction( + functionExpressionResult.value, + unparseAtomOrMolecule, + ) + } else { + const applyExpressionResult = readApplyExpression(value) + if (!either.isLeft(applyExpressionResult)) { + return unparseSugaredApply( + applyExpressionResult.value, + unparseAtomOrMolecule, + ) + } else { + const lookupExpressionResult = readLookupExpression(value) + if (!either.isLeft(lookupExpressionResult)) { + return unparseSugaredLookup( + lookupExpressionResult.value, + unparseAtomOrMolecule, + ) + } else { + return unparseSugarFreeMolecule(value, unparseAtomOrMolecule) + } + } + } + } + +export const quoteIfNecessary = (value: string): string => { + const unquotedAtomResult = unquotedAtomParser(value) + if ( + either.isLeft(unquotedAtomResult) || + unquotedAtomResult.value.remainingInput.length !== 0 + ) { + return quote.concat(escapeStringContents(value)).concat(quote) + } else { + return value + } +} + +export const serializeIfNeeded = ( + nodeOrMolecule: SemanticGraph | Molecule, +): Either => + isSemanticGraph(nodeOrMolecule) + ? serialize(nodeOrMolecule) + : either.makeRight(nodeOrMolecule) + +export const sugarFreeMoleculeAsKeyValuePairStrings = ( + value: Molecule, + unparseAtomOrMolecule: UnparseAtomOrMolecule, +): Either => { + const entries = Object.entries(value) + + const keyValuePairsAsStrings: string[] = [] + let ordinalPropertyKeyCounter = 0n + for (const [propertyKey, propertyValue] of entries) { + const valueAsStringResult = unparseAtomOrMolecule(propertyValue) + if (either.isLeft(valueAsStringResult)) { + return valueAsStringResult + } + + // Omit ordinal property keys: + if (propertyKey === String(ordinalPropertyKeyCounter)) { + keyValuePairsAsStrings.push(valueAsStringResult.value) + ordinalPropertyKeyCounter += 1n + } else { + keyValuePairsAsStrings.push( + kleur + .cyan(quoteIfNecessary(propertyKey).concat(colon)) + .concat(' ') + .concat(valueAsStringResult.value), + ) + } + } + return either.makeRight(keyValuePairsAsStrings) +} + +export const unparseAtom = (atom: string): Right => + either.makeRight( + quoteIfNecessary( + /^@[^@]/.test(atom) ? kleur.bold(kleur.underline(atom)) : atom, + ), + ) + +type UnparseAtomOrMolecule = ( + value: Atom | Molecule, +) => Either + +const escapeStringContents = (value: string) => + value.replace('\\', '\\\\').replace('"', '\\"') + +const unparseSugaredApply = ( + expression: ApplyExpression, + unparseAtomOrMolecule: UnparseAtomOrMolecule, +) => + either.flatMap(serializeIfNeeded(expression.function), serializedFunction => + either.flatMap( + unparseAtomOrMolecule(serializedFunction), + functionToApplyAsString => + either.flatMap( + serializeIfNeeded(expression.argument), + serializedArgument => + either.map( + unparseAtomOrMolecule(serializedArgument), + argumentAsString => + functionToApplyAsString + .concat(openParenthesis) + .concat(argumentAsString) + .concat(closeParenthesis), + ), + ), + ), + ) + +const unparseSugaredFunction = ( + expression: FunctionExpression, + unparseAtomOrMolecule: UnparseAtomOrMolecule, +) => + either.flatMap(serializeIfNeeded(expression.body), serializedBody => + either.map(unparseAtomOrMolecule(serializedBody), bodyAsString => + [kleur.cyan(expression.parameter), arrow, bodyAsString].join(' '), + ), + ) + +const unparseSugaredLookup = ( + expression: LookupExpression, + unparseAtomOrMolecule: UnparseAtomOrMolecule, +) => { + const keyPath = Object.entries(expression.query).reduce( + (accumulator: KeyPath | 'invalid', [key, value]) => { + if (accumulator === 'invalid') { + return accumulator + } else { + if (key === String(accumulator.length) && typeof value === 'string') { + return [...accumulator, value] + } else { + return 'invalid' + } + } + }, + [], + ) + + if ( + keyPath !== 'invalid' && + Object.keys(expression.query).length === keyPath.length && + keyPath.every(key => !either.isLeft(unquotedAtomParser(key))) + ) { + return either.makeRight(kleur.cyan(colon.concat(keyPath.join(dot)))) + } else { + return either.flatMap( + serializeIfNeeded(expression.query), + serializedKeyPath => + either.map(unparseAtomOrMolecule(serializedKeyPath), keyPathAsString => + kleur.cyan(colon.concat(keyPathAsString)), + ), + ) + } +} diff --git a/src/language/unparsing/pretty-json.ts b/src/language/unparsing/pretty-json.ts index 81f372f..f2b0967 100644 --- a/src/language/unparsing/pretty-json.ts +++ b/src/language/unparsing/pretty-json.ts @@ -16,7 +16,7 @@ const escapeStringContents = (value: string) => const key = (value: Atom): string => quote.concat(kleur.bold(escapeStringContents(value))).concat(quote) -const atom = (value: Atom): Right => +const unparseAtom = (value: Atom): Right => either.makeRight( quote.concat( escapeStringContents( @@ -26,7 +26,7 @@ const atom = (value: Atom): Right => ), ) -const molecule = (value: Molecule): Right => { +const unparseMolecule = (value: Molecule): Right => { const entries = Object.entries(value) if (entries.length === 0) { return either.makeRight(openBrace.concat(closeBrace)) @@ -36,7 +36,7 @@ const molecule = (value: Molecule): Right => { key(propertyKey) .concat(colon) .concat(' ') - .concat(atomOrMolecule(propertyValue).value), + .concat(unparseAtomOrMolecule(propertyValue).value), ) .join(comma.concat('\n')) @@ -50,7 +50,10 @@ const molecule = (value: Molecule): Right => { } } -const atomOrMolecule = (value: Atom | Molecule) => - typeof value === 'string' ? atom(value) : molecule(value) +const unparseAtomOrMolecule = (value: Atom | Molecule) => + typeof value === 'string' ? unparseAtom(value) : unparseMolecule(value) -export const prettyJson: Notation = { atom, molecule } +export const prettyJson: Notation = { + atom: unparseAtom, + molecule: unparseMolecule, +} diff --git a/src/language/unparsing/pretty-plz.ts b/src/language/unparsing/pretty-plz.ts index 4303edb..4987a8f 100644 --- a/src/language/unparsing/pretty-plz.ts +++ b/src/language/unparsing/pretty-plz.ts @@ -1,185 +1,39 @@ -import type { Right } from '@matt.kantor/either' -import either, { type Either } from '@matt.kantor/either' -import kleur from 'kleur' -import type { UnserializableValueError } from '../errors.js' +import either from '@matt.kantor/either' import type { Atom, Molecule } from '../parsing.js' -import { unquotedAtomParser } from '../parsing/atom.js' import { - isSemanticGraph, - readApplyExpression, - readFunctionExpression, - readLookupExpression, - serialize, - type KeyPath, - type SemanticGraph, -} from '../semantics.js' + closeBrace, + moleculeUnparser, + openBrace, + sugarFreeMoleculeAsKeyValuePairStrings, + unparseAtom, +} from './plz-utilities.js' import { indent, type Notation } from './unparsing-utilities.js' -// TODO: Share implementation details with inline plz notation. - -const dot = kleur.dim('.') -const quote = kleur.dim('"') -const colon = kleur.dim(':') -const openBrace = kleur.dim('{') -const closeBrace = kleur.dim('}') -const openParenthesis = kleur.dim('(') -const closeParenthesis = kleur.dim(')') -const arrow = kleur.dim('=>') - -const escapeStringContents = (value: string) => - value.replace('\\', '\\\\').replace('"', '\\"') - -const quoteIfNecessary = (value: string) => { - const unquotedAtomResult = unquotedAtomParser(value) - if ( - either.isLeft(unquotedAtomResult) || - unquotedAtomResult.value.remainingInput.length !== 0 - ) { - return quote.concat(escapeStringContents(value)).concat(quote) - } else { - return value - } -} - -const atom = (node: string): Right => - either.makeRight( - quoteIfNecessary( - /^@[^@]/.test(node) ? kleur.bold(kleur.underline(node)) : node, - ), - ) - -const molecule = ( - value: Molecule, -): Either => { - const functionExpressionResult = readFunctionExpression(value) - if (!either.isLeft(functionExpressionResult)) { - return sugaredFunction( - functionExpressionResult.value.parameter, - functionExpressionResult.value.body, - ) - } else { - const applyExpressionResult = readApplyExpression(value) - if (!either.isLeft(applyExpressionResult)) { - return sugaredApply( - applyExpressionResult.value.argument, - applyExpressionResult.value.function, - ) - } else { - const lookupExpressionResult = readLookupExpression(value) - if (!either.isLeft(lookupExpressionResult)) { - return sugaredLookup(lookupExpressionResult.value.query) - } else { - return sugarFreeMolecule(value) - } - } - } -} - -const sugaredLookup = (keyPathAsNode: Molecule | SemanticGraph) => { - const keyPath = Object.entries(keyPathAsNode).reduce( - (accumulator: KeyPath | 'invalid', [key, value]) => { - if (accumulator === 'invalid') { - return accumulator - } else { - if (key === String(accumulator.length) && typeof value === 'string') { - return [...accumulator, value] - } else { - return 'invalid' - } - } - }, - [], - ) - - if ( - keyPath !== 'invalid' && - Object.keys(keyPathAsNode).length === keyPath.length && - keyPath.every(key => !either.isLeft(unquotedAtomParser(key))) - ) { - return either.makeRight(kleur.cyan(colon.concat(keyPath.join(dot)))) - } else { - return either.flatMap(serializeIfNeeded(keyPathAsNode), serializedKeyPath => - either.map(atomOrMolecule(serializedKeyPath), keyPathAsString => - kleur.cyan(colon.concat(keyPathAsString)), - ), - ) - } -} - -const sugaredFunction = ( - parameterName: string, - body: Molecule | SemanticGraph, -) => - either.flatMap(serializeIfNeeded(body), serializedBody => - either.map(atomOrMolecule(serializedBody), bodyAsString => - [kleur.cyan(parameterName), arrow, bodyAsString].join(' '), - ), - ) - -const sugaredApply = ( - argument: Molecule | SemanticGraph, - functionToApply: Molecule | SemanticGraph, -) => - either.flatMap(serializeIfNeeded(functionToApply), serializedFunction => - either.flatMap( - atomOrMolecule(serializedFunction), - functionToApplyAsString => - either.flatMap(serializeIfNeeded(argument), serializedArgument => - either.map(atomOrMolecule(serializedArgument), argumentAsString => - functionToApplyAsString - .concat(openParenthesis) - .concat(argumentAsString) - .concat(closeParenthesis), - ), - ), - ), - ) - -const sugarFreeMolecule = (value: Molecule) => { - const entries = Object.entries(value) - if (entries.length === 0) { +const unparseSugarFreeMolecule = (value: Molecule) => { + if (Object.keys(value).length === 0) { return either.makeRight(openBrace + closeBrace) } else { - const keyValuePairsAsStrings: string[] = [] - let ordinalPropertyKeyCounter = 0n - for (const [propertyKey, propertyValue] of entries) { - const valueAsStringResult = atomOrMolecule(propertyValue) - if (either.isLeft(valueAsStringResult)) { - return valueAsStringResult - } - - // Omit ordinal property keys: - if (propertyKey === String(ordinalPropertyKeyCounter)) { - keyValuePairsAsStrings.push(valueAsStringResult.value) - ordinalPropertyKeyCounter += 1n - } else { - keyValuePairsAsStrings.push( - kleur - .cyan(quoteIfNecessary(propertyKey).concat(colon)) - .concat(' ') - .concat(valueAsStringResult.value), - ) - } - } - - return either.makeRight( - openBrace - .concat('\n') - .concat(indent(2, keyValuePairsAsStrings.join('\n'))) - .concat('\n') - .concat(closeBrace), + return either.map( + sugarFreeMoleculeAsKeyValuePairStrings(value, unparseAtomOrMolecule), + keyValuePairsAsStrings => + openBrace + .concat('\n') + .concat(indent(2, keyValuePairsAsStrings.join('\n'))) + .concat('\n') + .concat(closeBrace), ) } } -const serializeIfNeeded = ( - nodeOrMolecule: SemanticGraph | Molecule, -): Either => - isSemanticGraph(nodeOrMolecule) - ? serialize(nodeOrMolecule) - : either.makeRight(nodeOrMolecule) +const unparseAtomOrMolecule = (value: Atom | Molecule) => + typeof value === 'string' ? unparseAtom(value) : unparseMolecule(value) -const atomOrMolecule = (value: Atom | Molecule) => - typeof value === 'string' ? atom(value) : molecule(value) +const unparseMolecule = moleculeUnparser( + unparseAtomOrMolecule, + unparseSugarFreeMolecule, +) -export const prettyPlz: Notation = { atom, molecule } +export const prettyPlz: Notation = { + atom: unparseAtom, + molecule: unparseMolecule, +}