@pixture/engine
Version:
Pixture
535 lines • 20 kB
JavaScript
import spellu from './spellu-engine.mjs';
import scriptkit from './spellu-scriptkit.mjs';
import dom from './pixture-dom.mjs';
function print(...args) {
console.log(...args);
}
var pixture;
(function (pixture) {
function pluck(array, keyName) {
return array.reduce((object, item) => {
object[item[keyName]] = item;
return object;
}, {});
}
pixture.pluck = pluck;
function stepGeneratorOf(array) {
let index = 0;
return function* (condition) {
while (index < array.length && condition(array[index])) {
yield array[index++];
}
};
}
pixture.stepGeneratorOf = stepGeneratorOf;
})(pixture || (pixture = {}));
(function (pixture) {
function loadConfig() {
}
pixture.loadConfig = loadConfig;
})(pixture || (pixture = {}));
// namespace pixture {
// export class TemplateModule {
// constructor(
// tags: Dictionary<dom.Element>,
// ) {
// this.tags = tags
// }
// has(
// tagName: string
// ): boolean {
// return tagName.toLowerCase() in this.tags
// }
// get(
// tagName: string
// ): dom.Element {
// return this.tags[tagName.toLowerCase()]
// }
// tags: Dictionary<dom.Element>
// }
// export const enum TempmlateFormat {
// XML = "xml",
// }
// export function loadTemplate(
// format: TempmlateFormat,
// text: string,
// path: string,
// ): TemplateModule {
// const components = dom.loadTemplateElements(
// dom.parseDataDocument(text, path).root
// )
// return new TemplateModule(components)
// }
// }
(function (pixture) {
function generate(env, config) {
for (const recipe of config.pages) {
const input = pixture.load(env, config, recipe);
const output = pixture.process(input);
pixture.save(env, config, recipe, output);
}
return {};
}
pixture.generate = generate;
})(pixture || (pixture = {}));
(function (pixture) {
function transpile(page, components, dimensions, constructors) {
function unwrap(source) {
if (typeof source == "string") {
return [source, ""];
}
if (typeof source == "object") {
return [source.source, source.location];
}
else {
throw new Error(`Unrecognized argument "source"`);
}
}
const source = unwrap(page);
// ページドキュメントの準備
const pageDocument = dom.parsePageDocument(source[0], source[1]);
// ページコンポーネントの準備
let pageComponents = {
...components
.map(_ => unwrap(_))
.map(_ => dom.parseComponent(_[0], _[1]))
.reduce((obj, item) => ({
...obj, [item.tag]: item
}), {}),
// ページ内のComponentを抽出する
...dom.retrieveComponents(pageDocument.root),
};
// ページからtemplate要素を削除する
dom.removeComponentElements(pageDocument.root);
const input = {
dimensions,
constructors,
components: pageComponents,
document: pageDocument,
};
const output = pixture.process(input);
return output.text;
}
pixture.transpile = transpile;
})(pixture || (pixture = {}));
(function (pixture) {
function load(env, config, recipe) {
const documentData = loadJsonFile(config.data);
const componentLibrary = config.components.map(_ => loadXmlFile(_));
const pageDocument = loadHtmlFile(recipe.in);
const input = {
document: pageDocument,
components: dom.retrieveComponents(...componentLibrary.map(_ => _.root), pageDocument.root),
dimensions: [
{ id: "^", thing: env.universeGlobals() },
{ id: "~", thing: documentData },
],
constructors: env.universeConstructors(),
};
dom.removeComponentElements(pageDocument.root);
return input;
function loadJsonFile(path) {
return dom.parseData(env.readText(path), path);
}
function loadXmlFile(path) {
return dom.parseDataDocument(env.readText(path), path);
}
function loadHtmlFile(path) {
return dom.parsePageDocument(env.readText(path), path);
}
}
pixture.load = load;
})(pixture || (pixture = {}));
(function (pixture) {
function save(env, config, recipe, output) {
env.writeText(recipe.out, output.text);
const result = {};
return result;
}
pixture.save = save;
})(pixture || (pixture = {}));
(function (pixture) {
function process(input) {
const document = input.document;
const world = scriptkit.createWorld(input.dimensions, input.constructors);
pixture.applyComponent(world, document, input.components);
pixture.applyControl(world, document);
pixture.evaluateInterpolationCode(world, document.root, {});
const text = dom.buildPageDocument(document, {
pretty: true,
});
return {
document,
text,
};
}
pixture.process = process;
})(pixture || (pixture = {}));
(function (pixture) {
function applyComponent(world, page, components) {
const stack = [];
visitElement(page.root);
function visitElement(domElement) {
// 探索中の要素のタグ名が、コンポーネントに登録されていたら
if (domElement.tagName in components) {
if (stack.includes(domElement.tagName)) {
throw new Error("template recursive.");
}
stack.push(domElement.tagName);
apply(domElement, components[domElement.tagName]);
stack.pop();
}
// コンポーネントに登録されていなかったら
else {
for (const domNode of domElement.children) {
if (domNode instanceof dom.Element) {
visitElement(domNode);
}
}
}
}
function apply(target, component) {
// templateの適用
applyTemplate(target, component.template);
// styleの適用
applyStyle(target, component.style);
}
function applyTemplate(target, template, inline = false) {
const slots = sortSlotContent(target);
if (inline) {
const parent = target.parent;
instantiateChildNodes(template.ast, slots).map(_ => parent.insertBefore(_, target));
parent.removeChild(target);
}
else {
// console.log(2, template.ast)
instantiateChildNodes(template.ast, slots).map(_ => target.appendChild(_));
}
}
function instantiateChildNodes(template, slots) {
const nodes = [];
for (const child of template.children) {
nodes.push(...instantiateNode(child, slots));
}
return nodes;
}
function instantiateNode(template, slots) {
if (template instanceof dom.Element) {
// apply template
visitElement(template);
// apply slot
if (template.tagName.toLowerCase() === "slot") {
const name = template.attributes.name ?? "";
// defined slot or default slot
return slots[name] ?? instantiateChildNodes(template, {});
}
// clone this element
else {
// do shallow copy
const element = template.clone();
element.children.length = 0;
instantiateChildNodes(template, slots)
.map(_ => element.appendChild(_));
return [element];
}
}
else {
return [template.clone()];
}
}
/**
* コンポーネントのstyleを適用する。
*
* @param target
* @param style
* @returns
*/
function applyStyle(target, style) {
let content = style.content.trim();
// 中身がないstyleは出力しない
if (content.length == 0) {
return;
}
// namespace区切り文字をエスケープする
let cssTagName = target.tagName.replaceAll(":", "\\:");
// テンプレート文字列を展開する
content = pixture.applyString(world, {}, content);
// TODO: 指定言語でパース処理をする
// 擬似ターゲット :scope をタグ名に置き換える
// NOTE: とりあえずの実装:単純な文字列置換
content = content.replaceAll(":scope", cssTagName);
// head要素に追加する
page.appendStyle(content, { id: `__pixture_style_${target.tagName}` });
}
}
pixture.applyComponent = applyComponent;
function sortSlotContent(element) {
const slots = {};
for (const content of element.children) {
if (content instanceof dom.Element) {
if (content.tagName.toLowerCase() === "p-slot") {
const name = content.attributes.name ?? "";
for (const child of content.children) {
add(name, child);
}
}
else {
const name = content.attributes.slot ?? "";
add(name, content);
delete content.attributes.slot;
}
}
else {
add("", content);
}
}
while (element.children.length > 0) {
element.removeChild(element.children[0]);
}
return slots;
function add(name, node) {
if (name in slots) {
slots[name].push(node);
}
else {
slots[name] = [node];
}
}
}
})(pixture || (pixture = {}));
(function (pixture) {
const specialAttributeNames = ["data", "visible"];
function applyControl(world, page) {
for (const domNode of [...page.root.children]) {
if (domNode instanceof dom.Element) {
applyElement(domNode);
}
}
function applyElement(domElement) {
switch (domElement.tagName) {
case "p-branch":
applyBranch(domElement);
break;
case "p-each":
applyEach(domElement);
break;
default:
for (const domNode of [...domElement.children]) {
if (domNode instanceof dom.Element) {
applyElement(domNode);
}
}
}
function applyBranch(domElement) {
const parent = domElement.parent;
const newNodes = [];
let condition = null;
const gen = pixture.stepGeneratorOf([...domElement.children]);
for (const domElement of gen(node => node instanceof dom.Element && node.tagName == "p-if")) {
if (condition !== true) {
const result = pixture.evaluateExpression(world, {}, domElement.attributes.condition ?? "${false}");
if (typeof result != "boolean") {
throw new Error(`Attribute "condition" must boolean expression.`);
}
condition = result;
if (condition === true) {
newNodes.push(...domElement.children);
}
}
}
for (const domElement of gen(node => node instanceof dom.Element && node.tagName == "p-else")) {
if (condition === null) {
throw new Error("require <p-if>.");
}
if (condition === false) {
newNodes.push(...domElement.children);
}
}
parent.replaceChild(domElement, newNodes);
}
/**
* p:each要素をapplyする。
*
* @param domElement
*/
function applyEach(domElement) {
// TODO: check attribute.timing
const parent = domElement.parent;
// data属性のスクリプトを評価する
// console.log(1, world, "data=", domElement.attributes.data);
let result = pixture.evaluateExpression(world, {}, domElement.attributes.data ?? "[]");
// 1. ES Array
if (Array.isArray(result)) {
// OK
}
// 2. ES6 iteratable
// REF: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#iterator
else if (typeof result[Symbol.iterator] == "function") {
result = [...result];
}
else {
console.log(2, world, "data=", domElement.attributes.data, "type=", typeof result, result);
throw new Error(`data attribute must be array or iterator.`);
}
// item, else要素の決定
let itemElements = domElement.children.filter(_ => _ instanceof dom.Element && _.tagName == "p-item");
let elseElements = domElement.children.filter(_ => _ instanceof dom.Element && _.tagName == "p-else");
// 定義されていなければ、p:eachの子要素をitem要素として使う
if (itemElements.length == 0 && elseElements.length == 0) {
itemElements = [domElement];
}
const newNodes = [];
//- p:eachの子要素の展開
// p:item要素
if (result.length > 0) {
for (let index = 0; index < result.length; ++index) {
const template = findMatchedItem(itemElements, result, index);
if (template) {
newNodes.push(...createClone(template, result, index));
}
else {
console.log("<p-item> No matched");
}
}
}
// p:else要素
else {
for (const template of elseElements) {
newNodes.push(...createClone2(template));
}
}
parent.replaceChild(domElement, newNodes);
}
/**
* 要素配列から、condition属性の評価値がtrueのものをピックアップする。
*
* @param itemElements
* @param list
* @param index
* @returns
*/
function findMatchedItem(itemElements, list, index) {
for (const template of itemElements) {
// スクリプト内で使える変数の設定
const locals = {
...list[index],
"$$": list,
"$_": index,
};
// condition属性の評価
const result = pixture.evaluateExpression(world, locals, template.attributes.condition ?? "${true}");
// 戻り値の型チェック
if (typeof result != "boolean") {
throw new Error(`Attribute "condition" must boolean expression.`);
}
if (result)
return template;
}
return null;
}
/**
* vdom要素を複製する。
*
* @param itemElement
* @param data
* @returns
*/
function createClone(itemElement, list, index) {
// スクリプト内で使える変数の設定
const locals = list[index];
locals["$$"] = list;
locals["$_"] = index;
return itemElement.children.map(node => {
const another = node.clone();
evaluateInterpolationCode(world, another, locals);
return another;
});
}
/**
* vdom要素を複製する。
*
* @param itemElement
* @param data
* @returns
*/
function createClone2(itemElement) {
return itemElement.children.map(node => {
const another = node.clone();
evaluateInterpolationCode(world, another, {});
return another;
});
}
}
}
pixture.applyControl = applyControl;
/**
* ノードの子要素・テキストに対し、applyString()を実行する。
*
* @param world
* @param node
* @param context
*/
function evaluateInterpolationCode(world, node, context) {
const walker = new dom.ModelWalker(new class {
visitElementBefore(_, element) {
// apply attributes
}
visitElementAfter(_, element) {
for (const name in element.attributes) {
if (specialAttributeNames.includes(name))
continue;
element.attributes[name] = pixture.applyString(world, context, element.attributes[name], "aa1");
}
}
visitText(_, node) {
// apply text
node.text = pixture.applyString(world, context, node.text, "aa2");
}
});
walker.walkNode(undefined, node);
}
pixture.evaluateInterpolationCode = evaluateInterpolationCode;
})(pixture || (pixture = {}));
(function (pixture) {
function applyString(world, context, string, location) {
let scaned;
try {
scaned = scriptkit.scan({ input: string, location }, "scriptkit/template-string");
}
catch (e) {
console.log(91, e.name, e.message);
return string;
}
try {
return scriptkit.evaluate(world, context, scaned);
}
catch (e) {
console.log(92, e.name, e.message);
return "";
}
}
pixture.applyString = applyString;
})(pixture || (pixture = {}));
(function (pixture) {
function evaluateExpression(world, context, string, location) {
let scaned;
try {
scaned = scriptkit.scan({ input: string, location }, "scriptkit/template-expression");
}
catch (e) {
console.log(91, e.name, e.message);
return [];
}
try {
return scriptkit.evaluate(world, context, scaned);
}
catch (e) {
console.log(92, e.name, e.message);
return "";
}
}
pixture.evaluateExpression = evaluateExpression;
})(pixture || (pixture = {}));
export default pixture;
export { spellu, scriptkit, dom };
//# sourceMappingURL=pixture-engine.mjs.map