abi.js
Version:
[![typescript-icon]][typescript-link] [![license-icon]][license-link] [![status-icon]][status-link] [![ci-icon]][ci-link] [![twitter-icon]][twitter-link]
471 lines (467 loc) • 12.7 kB
JavaScript
// src/view.test.ts
import { expect, test } from "bun:test";
// src/parser.ts
function parse_str(val) {
val = val.trim();
const res = /^('|")(.*?)\1$/gms.exec(val);
if (res) {
val = res[2];
}
return val;
}
// src/view.ts
var Doc = class {
constructor(root, type = "html", charset = "UTF-8", version, mode) {
this.root = root;
this.type = type;
this.charset = charset;
if (version === void 0) {
this.version = this.type === "html" ? 5 : 1;
} else {
this.version = version;
}
if (mode === void 0) {
this.mode = "strict";
} else {
this.mode = mode;
}
}
version;
mode;
render(locale) {
let str = "";
switch (this.type) {
case "xml":
str += `<?xml version="${this.version.toFixed(1)}" encoding="${this.charset}"?>
`;
break;
case "xhtml":
if (this.version === 1.1) {
str += `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">`;
} else if (this.version === 1) {
switch (this.mode) {
case "strict":
str += `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">`;
break;
case "frameset":
str += `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">`;
break;
default:
str += `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">`;
}
}
break;
case "html":
if (this.version >= 5) {
str += "<!DOCTYPE html>";
} else if (this.version === 4.01) {
switch (this.mode) {
case "strict":
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">';
break;
case "frameset":
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">';
break;
default:
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
}
} else if (this.version === 4) {
switch (this.mode) {
case "strict":
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">';
break;
case "frameset":
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" "http://www.w3.org/TR/REC-html40/frameset.dtd">';
break;
default:
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">';
}
} else if (this.version === 3.2) {
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">';
} else if (this.version === 2) {
str += '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">';
} else if (this.version === 1) {
str += '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 1.0//EN">';
}
break;
default:
throw new Error(`Unsupported document type: ${this.type}`);
}
return str + this.root.render(locale);
}
};
var Node = class {
};
var Tag = class extends Node {
constructor(name, ...nodes) {
super();
this.name = name;
this.addNodes(nodes);
}
nodes = [];
addNodes(nodes) {
for (const node of nodes) {
this.addNode(node);
}
return this;
}
addNode(node) {
this.nodes.push(node);
return this;
}
getNodes() {
return this.nodes;
}
getTexts() {
const texts = [];
for (const node of this.nodes) {
if (node instanceof Text) {
texts.push(node);
}
}
return texts;
}
getElements() {
const elements = [];
for (const node of this.nodes) {
if (node instanceof Element) {
elements.push(node);
}
}
return elements;
}
get slot() {
return new Slot(this.nodes);
}
get is_empty() {
return this.slot.is_empty;
}
render(locale) {
return this.open() + this.renderSlot(locale) + this.close();
}
renderSlot(locale) {
return this.slot.render(locale);
}
};
var Component = class extends Tag {
constructor(name, props, ...nodes) {
super(name, ...nodes);
this.props = props;
}
open() {
return `<${this.name}${this.renderProps()}${this.slot.is_empty ? "" : ">"}`;
}
close() {
return this.slot.is_empty ? "/>" : `</${this.name}>`;
}
renderProps() {
let props = "";
for (const prop of Object.entries(this.props)) {
props += ` ${prop[0]}="${prop[1].toString()}"`;
}
return props;
}
};
var Element = class _Element extends Tag {
constructor(name, attrs = {}, ...nodes) {
super(name, ...nodes);
this.attrs = attrs;
}
static ORPHAN = [
"area",
"base",
"basefont",
"br",
"col",
"command",
"embed",
"frame",
"hr",
"img",
"input",
"isindex",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
];
static INLINE = [
"a",
"abbr",
"acronym",
"b",
"bdi",
"bdo",
"big",
"br",
"cite",
"code",
"data",
"del",
"dfn",
"em",
"font",
"i",
"img",
"ins",
"kbd",
"map",
"mark",
"object",
"q",
"rp",
"rt",
"rtc",
"ruby",
"s",
"samp",
"small",
"span",
"strike",
"strong",
"sub",
"sup",
"time",
"tt",
"u",
"var"
];
renderAttrs() {
let attrs = "";
for (const attr of Object.entries(this.attrs)) {
attrs += ` ${attr[0]}="${attr[1]}"`;
}
return attrs;
}
open() {
return `<${this.name}${this.renderAttrs()}>`;
}
close() {
return this.is_orphan ? "" : `</${this.name}>`;
}
get is_orphan() {
return _Element.ORPHAN.includes(this.name);
}
get is_paired() {
return !this.is_orphan;
}
get is_inline() {
return _Element.INLINE.includes(this.name);
}
get is_block() {
return !this.is_inline;
}
get is_custom() {
return this.name.includes("-");
}
};
var Slot = class extends Node {
constructor(nodes) {
super();
this.nodes = nodes;
}
get is_empty() {
return this.nodes.length === 0;
}
render(locale) {
let str = "";
for (const node of this.nodes) {
str += node.render(locale);
}
return str;
}
};
var Text = class _Text extends Node {
constructor(value, translations = {}) {
super();
this.value = value;
_Text.setTranslations(value, translations);
}
static locale = "en_US";
static dictionnary = {};
static setTranslations(value, translations) {
for (const translation of Object.entries(translations)) {
_Text.setTranslation(value, translation[0], translation[1]);
}
}
static setTranslation(value, locale, translation) {
if (_Text.dictionnary[value] === void 0) {
_Text.dictionnary[value] = {};
}
_Text.dictionnary[value][locale] = translation;
}
static getTranslation(value, locale) {
return _Text.getTranslations(value)[locale];
}
static getTranslations(value) {
const translations = _Text.dictionnary[value];
if (!translations) {
for (const [defaultValue, translations2] of Object.entries(
_Text.dictionnary
)) {
for (const translation of Object.values(translations2)) {
if (value === translation) {
translations2[_Text.locale] = defaultValue;
return translations2;
}
}
}
}
return translations;
}
static translate(value) {
const translations = _Text.getTranslations(value);
return Translate.from(translations);
}
translateTo(locale) {
return _Text.getTranslation(this.value, locale) ?? this.value;
}
render(locale) {
return locale ? this.translateTo(locale) : this.value;
}
};
var Translate = class _Translate {
constructor(translations) {
this.translations = translations;
}
static from(translations) {
return new _Translate(translations);
}
to(locale) {
return this.translations[locale];
}
};
var Template = class {
constructor(content) {
this.content = content;
}
render(locale) {
const content = this.content.toString();
const ID = "[a-zA-Z]+[a-zA-Z0-9-_]*";
const SINGLE_QUOTE_STR = "'(?:\\'|[^'])*'";
const DOUBLE_QUOTE_STR = '"(?:\\"|[^"])*"';
const STR = `${SINGLE_QUOTE_STR}|${DOUBLE_QUOTE_STR}`;
const ATTR = `(${ID})\\s*=\\s*(${STR})\\s*`;
const ATTRS = `${ATTR}(?:\\s*${ATTR})*`;
const ATTRS_BLOCK = `\\[(${ATTRS})\\]`;
const ELT = `(${ID})\\s*${ATTRS_BLOCK}`;
const ELT_BLOCK = `${ELT}\\s*\\{\\s*(.*?)\\s*\\}`;
const elt_m = RegExp(ELT_BLOCK).exec(content);
if (elt_m) {
const attrs = {};
let attrs_str = elt_m[2] || "";
let attrs_m = RegExp(ATTR, "gm").exec(attrs_str);
while (attrs_m) {
attrs[attrs_m[1]] = parse_str(attrs_m[2]);
attrs_str = attrs_str.replace(attrs_m[0], "");
attrs_m = RegExp(ATTR, "gm").exec(attrs_str);
}
const elt = element(elt_m[1], attrs, text(elt_m[7]));
return elt.render(locale);
}
return content;
}
};
function doc(root, type = "html", charset = "UTF-8", version, mode) {
return new Doc(root, type, charset, version, mode);
}
function text(value, translations = {}) {
return new Text(value, translations);
}
function component(name, props = {}, ...nodes) {
return new Component(name, props, ...nodes);
}
function element(name, attrs = {}, ...nodes) {
return new Element(name, attrs, ...nodes);
}
function template(content) {
return new Template(content);
}
// src/view.test.ts
test("Test doc", () => {
const _doc = doc(element("html"), "html");
expect(_doc).toBeInstanceOf(Doc);
expect(_doc.root).toBeInstanceOf(Node);
expect(_doc.render()).toEqual("<!DOCTYPE html><html></html>");
});
test("Test text", () => {
const txt = text("Hello", {
fr_CI: "Salut"
});
expect(txt).toBeInstanceOf(Text);
expect(txt).toBeInstanceOf(Node);
expect(txt.value).toEqual("Hello");
expect(txt.render()).toEqual("Hello");
expect(txt.render("fr_CI")).toEqual("Salut");
expect(txt.translateTo("fr_CI")).toEqual("Salut");
expect(Text.translate("Salut").to("en_US")).toEqual("Hello");
expect(Text.translate("Salut").to("fr_CI")).toEqual("Salut");
});
test("Test component", () => {
const cmp = component(
"MyHello",
{
name: "Sigui",
age: 27
},
element("p", {}, text("Hello"), element("b", {}, text("Sigui"))),
element("p", {}, text("Age"), element("b", {}, text("25")))
);
expect(cmp).toBeInstanceOf(Component);
expect(cmp).toBeInstanceOf(Node);
expect(cmp).toBeInstanceOf(Tag);
expect(cmp.slot).toBeInstanceOf(Slot);
expect(cmp.slot).toBeInstanceOf(Node);
expect(cmp.is_empty).toBeFalse();
expect(cmp.slot.render()).toEqual(
"<p>Hello<b>Sigui</b></p><p>Age<b>25</b></p>"
);
expect(cmp.render()).toEqual(
'<MyHello name="Sigui" age="27"><p>Hello<b>Sigui</b></p><p>Age<b>25</b></p></MyHello>'
);
});
test("Test element", () => {
const elt = element(
"div",
{
id: "MyDiv"
},
text("Hello"),
element("b", {}, text("World"))
);
expect(elt).toBeInstanceOf(Element);
expect(elt).toBeInstanceOf(Node);
expect(elt).toBeInstanceOf(Tag);
expect(elt.slot).toBeInstanceOf(Slot);
expect(elt.slot).toBeInstanceOf(Node);
expect(elt.is_empty).toBeFalse();
expect(elt.slot.render()).toEqual("Hello<b>World</b>");
expect(elt.render()).toEqual('<div id="MyDiv">Hello<b>World</b></div>');
});
test("Test inline element", () => {
const elt = element("br");
const c_elt = element("br", {}, text("content"));
expect(elt.is_inline).toBeTrue();
expect(c_elt.is_inline).toBeTrue();
expect(elt.is_empty).toBeTrue();
expect(c_elt.is_empty).toBeFalse();
expect(elt.is_paired).toBeFalse();
expect(c_elt.is_paired).toBeFalse();
expect(elt.is_custom).toBeFalse();
});
test("Test paired element", () => {
const elt = element("p");
expect(elt.is_inline).toBeFalse();
expect(elt.is_empty).toBeTrue();
expect(elt.is_paired).toBeTrue();
expect(elt.is_custom).toBeFalse();
});
test("Test template", () => {
const tpl = template`p[id="myP" class="my-4 mx-8" title='It\'s a Hello World'] { Bonjour le monde }`;
const r = `<p id="myP" class="my-4 mx-8" title="It's a Hello World">Bonjour le monde</p>`;
expect(tpl).toBeInstanceOf(Template);
expect(tpl.render()).toEqual(r);
});