UNPKG

sml-markup

Version:

A simplistic markup language inspired by XML

670 lines (615 loc) 18.2 kB
"use strict"; const lustils = require("lustils"); const dom = require("./dom.js"); const { unescape_sets, escape_sets } = require("./escape_sets.js"); const { Element, Document, element_builders } = dom; function isHexDigit(c) { return (c >= "0" && c <= "9") || (c >= "a" && c <= "f") || (c >= "A" && c <= "F"); } function isLetter(c) { return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); } const { InputError, SyntaxError } = lustils.error; const { StringBuilder, StreamLocator, obtainInputStream, obtainOutputStream, } = lustils.stream; const State = lustils.parse.MealyState; const SpecialState = lustils.parse.MealySpecialState; const MachineBase = lustils.parse.ExtendedMealyMachineBase; const Machine = lustils.parse.ExtendedMealyMachine; let content = new State(); content.setAnyTransition({ state: content }); let tag_reader = new MachineBase(undefined, content); let tag_error = new State(); let escape_error = new State(); let close_or_autoclose = new SpecialState(); let opening = new SpecialState(); let comment_incoming = new State(); let comment_incoming_2 = new State(); let comment = new State(); comment.setAnyTransition({ state: comment }); comment_incoming.setTransition("-", { state: comment_incoming_2 }); comment_incoming_2.setTransition("-", { state: comment }); let comment_outcomings = [comment]; let ending = "-->"; for (let i = 0; i < ending.length; i++) { let co = new State(); co.setAnyTransition({ state: comment }); comment_outcomings.push(co); comment_outcomings[i].setTransition(ending.charAt(i), { state: comment_outcomings[i + 1] }); } let comment_finished = comment_outcomings[comment_outcomings.length - 1]; comment_incoming.setAnyTransition({ state: tag_error }); let tag = new SpecialState(); content.setTransition("<", { state: tag }); let escape = new SpecialState(); content.setTransition("&", { state: escape }); let id_escape = new SpecialState(); let identifier_escape = new State(); id_escape.setConsumer((_machine, c) => { if (isLetter(c)) { return { state: id_escape, write: true }; } else if (c === ";") { return { state: identifier_escape }; } else { return { state: escape_error }; } }); let num_escape = new SpecialState(); let numeric_escape = new State(); num_escape.setConsumer((_machine, c) => { if (isHexDigit(c)) { return { state: num_escape, write: true }; } else if (c === ";") { return { state: numeric_escape }; } else { return { state: escape_error }; } }); escape.setConsumer((_machine, c) => { if (c === "#") { return { state: num_escape }; } else if (isLetter(c)) { return { state: id_escape, write: true }; } else { return { state: tag_error }; } }); tag.setConsumer((_machine, c) => { if (c === "/") { return { state: close_or_autoclose }; } else if (isLetter(c)) { return { state: opening, write: true }; } else if (c === "!") { return { state: comment_incoming }; } else { return { state: tag_error }; } }); let self_enclosing = new State(); let opened = new State(); opening.setConsumer((_machine, c) => { if (c === "/") { return { state: self_enclosing }; } else if (isLetter(c)) { return { state: opening, write: true }; } else if (c === ">") { return { state: opened }; } else { return { state: tag_error }; } }); let self_enclosed = new State(); self_enclosing.setTransition(">", { state: self_enclosed }); self_enclosing.setAnyTransition({ state: tag_error }); let autoclose = new State(); let close_name = new SpecialState(); close_or_autoclose.setConsumer((_machine, c) => { if (c === ">") { return { state: autoclose }; } else if (isLetter(c)) { return { state: close_name, write: true }; } else { return { state: tag_error }; } }); let closed = new State(); close_name.setConsumer((_machine, c) => { if (c === ">") { return { state: closed }; } else if (isLetter(c)) { return { state: close_name, write: true }; } else { return { state: tag_error }; } }); function error(message, stream) { throw new SyntaxError(message + " at " + stream.row + ":" + stream.col); } let readers = {}; const reader_confs = { default: { strict_tag: false, strict_comment: true, strict_escape: true, closing: "forcing_searching", escape_set: "html" }, strict: { strict_tag: true, strict_comment: true, strict_escape: true, closing: "strict", escape_set: "minimal" }, ignore: { strict_tag: false, strict_comment: false, strict_escape: false, closing: "forcing_searching", escape_set: "html" }, }; function reader(conf) { if (typeof (conf) !== "object") { return readers[conf] || readers.default; } conf = Object.freeze(lustils.object.complete(conf || {}, reader_confs.default)); const { strict_tag, strict_comment, strict_escape, closing, escape_set } = conf; const ElementBuilder = element_builders[closing] || element_builders.strict; const escapes = unescape_sets[escape_set] || unescape_sets.minimal; function read(stream) { stream = new StreamLocator(obtainInputStream(stream)); let doc = new ElementBuilder(new Document()); let c; let tag_name = new StringBuilder(); let machine = new Machine(tag_reader, tag_name); let text_content = ""; // eslint-disable-next-line no-cond-assign while (c = stream.read()) { machine.consume(c, tag_name); if (machine.current_state === tag || machine.current_state === escape) { doc.appendText(text_content); text_content = ""; } reset: { if (machine.current_state === closed) { doc.closeElement(tag_name.text); } else if (machine.current_state === opened) { doc.openChild(new Element(tag_name.text)); } else if (machine.current_state === self_enclosed) { doc.appendChild(new Element(tag_name.text, false)); } else if (machine.current_state === autoclose) { doc.closeActive(); } else if (machine.current_state === identifier_escape) { let char = escapes[tag_name.text]; if (char === undefined) { if (strict_escape) { error("invalid escape sequence", stream); } } else { doc.appendText(char); } } else if (machine.current_state === numeric_escape) { let num = Number.parseInt(tag_name.text, 16); if (num > 0x10FFFF) { if (strict_escape) { error("invalid escape sequence", stream); } } else { doc.appendText(String.fromCodePoint(num)); } } else if (machine.current_state === comment_finished) { // comment finished } else if (machine.current_state === tag_error) { if (strict_tag) { error("invalid tag", stream); } doc.appendText(text_content); } else if (machine.current_state === escape_error) { if (strict_tag) { error("invalid escape sequence", stream); } doc.appendText(text_content); } else { break reset; } tag_name = new StringBuilder(); text_content = ""; machine = new Machine(tag_reader, tag_name); continue; } text_content += c; } if (machine && machine.current_state === comment && strict_comment) { error("unclosed comment", stream); } doc.appendText(text_content); return doc.build(); } const reader = { conf, read }; Object.freeze(reader); return reader; } for (let r_conf in reader_confs) { readers[r_conf] = reader(reader_confs[r_conf]); } const writer_confs = { default: { indent: undefined, autoclose: false, spacing: "", escape_set: "minimal" }, compress: { autoclose: true }, beautify: { indent: " ", spacing: "\n", } }; let writers = {}; function writer(conf) { if (typeof (conf) !== "object") { return writers[conf] || writers.default; } conf = Object.freeze(lustils.object.complete(conf, writer_confs.default)); const { indent, autoclose, spacing, escape_set } = conf; const escapes = escape_sets[escape_set] || escape_sets.minimal; function writeIndent(level, out) { if (!indent) { return; } for (let i = 0; i < level; i++) { out.write(indent); } } function _write(elem, out, level) { out.write(spacing); for (let child of elem.content) { writeIndent(level, out); if (typeof (child) === "string") { for (let c of child) { let e; if ((e = escapes[c]) !== undefined) { out.write("&"); out.write(e); out.write(";"); } else { let code = c.charCodeAt(0); if (code <= 31 || code === 127) { out.write("&"); out.write(code.toString(16)); out.write(";"); } } } out.write(child); } else { out.write("<"); out.write(child.name); out.write(">"); _write(child, out, level + 1); out.write("</"); if (!autoclose) { out.write(child.name); } out.write(">"); } out.write(spacing); } } function write(doc, out) { out = obtainOutputStream(out); _write(doc, out, 0); return out; } const writer = { conf, write }; Object.freeze(writer); return writer; } for (let w_conf in writer_confs) { writers[w_conf] = writer(writer_confs[w_conf]); } function isInvalidEntry(entry) { return entry.name !== "entry" || entry.content.length !== 2 || entry.content[1].name !== "value" || entry.content[1].content.length !== 1; } function readObj(obj) { if (typeof (obj) === "string") { return obj; } else { if (obj.name === "nil") { return undefined; } if (obj.name === "true") { return true; } if (obj.name === "false") { return false; } if (obj.name === "text") { if (obj.content.length === 1) { return ""; } else if (obj.content.length === 1) { return obj.content[0]; } else { throw new InputError("invalid text"); } } if (obj.name === "number") { if (obj.content.length === 1) { return Number.parseFloat(obj.content[0]); } else { throw new InputError("invalid number"); } } let result; if (obj.name === "set") { result = new Set(); for (let elem of obj.content) { result.add(readObj(elem)); } } else if (obj.name === "list") { result = []; for (let elem of obj.content) { result.push(readObj(elem)); } } else if (obj.name === "map") { result = new Map(); for (let entry of obj.content) { if (isInvalidEntry(entry)) { throw new InputError("invalid entry"); } result.set(readObj(entry.content[0]), entry.content[1].content[0]); } } else if (obj.name === "dictionairy") { result = {}; for (let entry of obj.content) { if (isInvalidEntry(entry)) { throw new InputError("invalid entry"); } result[readObj(entry.content[0])] = entry.content[1].content[0]; } } return result; } } // Note: Right now there is no usage for conf, but there may be later, and the below code ensures backwards compat later on let object_readers = {}; const object_reader_confs = { default: {} }; function objectReader(conf) { if (typeof (conf) !== "object") { return object_readers[conf] || object_readers.default; } conf = Object.freeze(lustils.object.complete(conf, object_reader_confs.default)); function read(doc) { if (doc.content.length === 1) { return readObj(doc.content[0]); } else if (doc.content.length > 1) { let list = []; for (let element of doc.content) { list.push(element); } return list; } throw new InputError("Empty document"); } const reader = { conf, read }; Object.freeze(reader); return reader; } for (let or_conf in object_reader_confs) { object_readers[or_conf] = objectReader(object_reader_confs[or_conf]); } let object_writers = {}; const object_writer_confs = { default: { write_function: "writeSML" }, strict: { write_function: undefined } }; function objectWriter(conf) { if (typeof (conf) !== "object") { return object_writers[conf] || object_writers.default; } conf = Object.freeze(lustils.object.complete(conf, object_writer_confs.default)); const write_function = conf.write_function; function _writeMap(map, parent) { for (let key in map) { let elem = new Element("entry"); elem.appendChild(write(key)); let value = new Element("value"); value.appendChild(write(value)); elem.appendChild(value); parent.appendChild(elem); } } function write(object) { let element; if (object === true) { element = new Element("true", false); } else if (object === false) { element = new Element("false", false); } else if (object === undefined || object === null) { element = new Element("nil", false); } else { let t = typeof (object); if (t === "string") { element = new Element("text"); element.appendText(object); } else if (t === "number") { let element = new Element("number"); element.appendText(object.toString(10)); } else if (t === "object") { if (Array.isArray(object)) { element = new Element("list"); for (let elem of object) { element.appendChild(write(elem)); } } else if (object instanceof Set) { element = new Element("set"); for (let elem of object) { element.appendChild(write(elem)); } } else if (object instanceof Map) { element = new Element("map"); _writeMap(object, element); } else { let wfunc; if (write_function && (wfunc = object[write_function]) && typeof (object(wfunc)) === "function") { element.content.push(wfunc()); } else { element = new Element("dictionairy"); _writeMap(object, element); } } } else { throw new InputError("invalid object"); } } return element; } const writer = { conf, write, }; return Object.freeze(writer); } for (let ow_conf in object_writer_confs) { object_writers[ow_conf] = objectWriter(object_writer_confs[ow_conf]); } let exps = { reader, objectReader, readObject: objectReader().read, read: reader().read, readStrict: reader("strict").read, readIgnore: reader("ignore").read, writer, write: writer().write, writeCompressed: writer("compress").write, writeBeautified: writer("beautify").write, objectWriter, writeObject: objectWriter().write, writeObjectStrict: objectWriter("strict").write }; for (let write_func of ["write", "writeCompressed", "writeBeautified"]) { exps[write_func + "Text"] = object => exps[write_func](object).text; } for (let e in dom) { exps[e] = dom[e]; } module.exports = exps;