UNPKG

@pixture/engine

Version:

Pixture

535 lines 20 kB
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