majas
Version:
Format-agnostic structure converter.
167 lines (166 loc) • 5.96 kB
JavaScript
import * as Filesystem from '../formats/Filesystem.js';
import * as IR from '../formats/IR.js';
import * as Markdown from '../formats/Markdown.js';
import {} from './Format.js';
import { Ajv } from 'ajv';
import { wrapValidatorAsTypeGuard } from 'json-schema-to-ts';
import { decodeRawInput } from './Formatter.js';
import * as fstree from '../fstree.js';
import { stdout } from 'process';
import { createWriteStream } from 'fs';
import squrlyFormat from 'squrly-format';
import { map } from '../util/misc.js';
import { EOL } from 'os';
const ajv = new Ajv({
useDefaults: true,
});
const validate = wrapValidatorAsTypeGuard((schema, data) => ajv.validate(schema, data));
const encoding = {
enum: [
'ascii',
'utf8',
'utf-8',
'utf16le',
'utf-16le',
'ucs2',
'ucs-2',
'base64',
'base64url',
'latin1',
'binary',
'hex',
],
description: 'text encoding to use',
default: 'utf-8',
};
const optionSchemas = {
ir: {
encoding,
space: {
oneOf: [
{
type: 'string',
},
{ type: 'integer', minimum: 0 },
],
description: 'Adds indentation, white space, and line break characters to the output JSON text to make it easier to read. Same syntax as the JSON.stringify space argument',
},
},
md: {
encoding,
depth: {
enum: Markdown.HeadingDepths,
description: 'Maximum depth of headings to parse. Headings deeper than this limit will be included as regular content.',
default: Markdown.DefaultOptions.depth,
},
},
fs: {
encoding,
basedir: {
type: 'string',
description: 'Base dirname to place the root node children in. May be a path.',
default: Filesystem.DefaultOptions.baseDirname,
},
basefile: {
type: 'string',
description: 'Base filename to place root node content in. May be a path. Extension not included.',
default: Filesystem.DefaultOptions.baseFilename,
},
replace: {
type: 'string',
description: 'String to replace invalid filename characters in node titles with.',
},
header: {
type: 'string',
description: 'squrly-format replacement pattern for file headers. {title} is replaced by the node title. {index} is replaced by the node index. {n} is replaced by a newline.',
},
filename: {
type: 'string',
description: 'squrly-format replacement pattern for file names. {title} is replaced by the node title. {index} is replaced by the node index. {n} is replaced by a newline.',
}
},
};
export default [
{
displayName: 'IR',
aliases: [],
fileExtensions: [],
optionsSchema: optionSchemas.ir,
accepts: 'IRNode JSON',
emits: 'IRNode JSON',
create(options) {
const o = parseOptions(options, optionSchemas.ir);
const irFormatter = new IR.default(this, o.space);
return {
parse(input) {
return irFormatter.parse(decodeRawInput(input, o.encoding));
},
emit(output, location) {
textEmit(location, irFormatter.emit(output));
},
};
},
},
{
displayName: 'Markdown',
fileExtensions: ['md', 'markdown', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mkdown', 'ron'],
aliases: ['md'],
optionsSchema: optionSchemas.md,
accepts: 'markdown markup text',
emits: 'markdown markup text',
create(options) {
const o = parseOptions(options, optionSchemas.md);
const mdFormatter = new Markdown.default(this, o);
return {
parse(input) {
return mdFormatter.parse(decodeRawInput(input, o.encoding));
},
emit(output, location) {
textEmit(location, mdFormatter.emit(output));
},
};
},
},
{
displayName: 'Filesystem',
aliases: ['fs'],
fileExtensions: [],
optionsSchema: optionSchemas.fs,
accepts: 'a filesystem path',
emits: 'a filesystem subtree at the output location or the current directory if unspecified.',
create(options) {
const o = parseOptions(options, optionSchemas.fs);
const fsFormatter = new Filesystem.default(this, {
baseDirname: o.basedir,
baseFilename: o.basefile,
invalidFilenameCharReplacement: o.replace,
header: map(fmt => (title, index) => squrlyFormat(fmt, { title, index, n: EOL }), o.header),
filename: map(fmt => (title, index) => squrlyFormat(fmt, { title, index, n: EOL }), o.filename),
});
return {
parse(input) {
const title = decodeRawInput(input, o.encoding, x => x);
return fsFormatter.parse({
title,
children: fstree.read(title, o.encoding),
});
},
emit(output, location) {
fstree.write(fsFormatter.emit(output), location ?? '.');
},
};
},
},
];
function textEmit(location, output) {
const f = location === undefined ? stdout : createWriteStream(location);
f.write(output);
}
function parseOptions(options, schema) {
if (validate({ type: 'object', properties: schema, additionalProperties: false }, options)) {
return options;
}
else {
throw new Error(`Invalid options: ${ajv.errorsText(ajv.errors)}: ${JSON.stringify(options)}`);
}
}