ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
dir: /syntax.js/
import {append, complement, MakeBlock, replace} from "./model.js" import {TextParameter, BooleanParameter, BooleanSlot, Variable, List, Forever, Define, Custom, compareName, TextSlot} from "./core.js" function escape(text) { text = text.replace(/[^-a-zA-Z0-9 _#/]/gu, n => "\\" + n) if (text.endsWith(" v")) text = text.slice(0, -1) + "\\v" return text } function unescape(text) { return text.replace(/\\./g, n => n.slice(1)) } function toName(name) { return name.normalize().replace(/\s+/ug, " ").trim() } let braces = { "(": ")", "[": "]", "<": ">", "{": "}", } function toBareSyntax(block, options) { let inputs = [] for (let i of block.type.references?.keys() ?? []) inputs.push(`[${escape(block.names[i])} v]`) inputs.push(...block.inputs.map(input => toSyntax0(input, options))) let name = options.getName?.(block.type, ...inputs) ?? block.type.name(...inputs) let array = name.map(part => { if (part instanceof Array) part = part.find(v => typeof v === "string") ?? "" if (typeof part === "string") return part if (part instanceof Element) return for (let option of part.options) { let type = option.type ?? option.morph(block).type if (block.type === type) return `[${escape(option.label)} v]` } }) return array.filter(Boolean) } function toReporterSyntax(block, options) { if (block.type.shape === "slot") { if (block.type.options) { let name = options.getName?.(block.type, block.value) ?? block.type.name?.(block.value) ?? block.value if (block.type.canFit) return `[${escape(name)} v]` return `(${escape(name)} v)` } if (block.type === BooleanSlot.type) return "<>" if (block.type.numeric) return `(${block.value})` let value = block.value.replaceAll("]", "\\]") return `[${escape(value)}]` } if (block.type.shape === "reporter" && block.type.output === "boolean") return `<${toBareSyntax(block, options).join(" ")}>` return `(${toBareSyntax(block, options).join(" ")})` } function toCustomBlockSyntax(block, options) { let name = [] let i = 0 for (let part of block.names[0]) { if (typeof part === "string") { name.push(part) continue } if (block.type === Define.type) { if (part.type === "boolean") name.push(`<${escape(part.name)}>`) else name.push(`(${escape(part.name)})`) continue } let input = block.inputs[i++] let text = toSyntax0(input, options) if (input.type.output === "boolean" && part.type === "text") text = `(${text})` name.push(text) } return name } function toDefineSyntax(prototype, options) { return (options.getName?.(Define.type, prototype) ?? Define.type.name(prototype)).map(part => typeof part === "string" ? toName(part) : part).filter(Boolean) } function toDefineRegex(options) { let symbol = Symbol() return new RegExp(`^\\s*${toDefineSyntax(symbol, options).flatMap(part => part === symbol ? ["(.+)"] : part.split(/\s+/g).filter(Boolean).map(part => RegExp.escape(part))).join("\\s+")}\\s*$`, "u") } function wrapPhonyDefine(parts, extra, options) { if (parts.map(part => typeof part === "string" ? part : "(())").join(" ").match(toDefineRegex(options))) { extra = [...extra, "stack"] } if (extra.length > 0) return parts.join(" ") + " :: " + extra.join(" ") return parts.join(" ") } function toSyntax0(block, options) { if (block.type === Variable.type || block.type === List.type || block.type === TextParameter.type) { return `(${escape(block.names[0])} :: ${block.type.category})` } if (block.type === BooleanParameter.type) { return `<${escape(block.names[0])} :: custom>` } if (block.type === Define.type) { let syntax = toDefineSyntax(toCustomBlockSyntax(block, options).join(" "), options).join(" ") if (block.atomic) return syntax + " // atomic" return syntax } if (block.type.category === "custom") return wrapPhonyDefine(toCustomBlockSyntax(block, options), ["custom"], options) if (block.type.shape === undefined) return wrapPhonyDefine(toBareSyntax(block, options), [], options) return toReporterSyntax(block, options) } export function toSyntax(block, options = {}) { let syntax = toSyntax0(block, options) if (block.type.hat) { if (block.stack.length > 0) syntax += "\n" + toSyntax(block.stack[0], options) return syntax } if (!block.parent || block.type.shape !== undefined) return syntax if (block.stack) { syntax += "\n" if (block.stack.length > 0) { syntax += toSyntax(block.stack[0], options).replace(/^/mg, "\t") + "\n" } } let block0 = block while (block0.complement) { block0 = block0.complement syntax += toSyntax0(block0, options) + "\n" if (block0.stack.length > 0) { syntax += toSyntax(block0.stack[0], options).replace(/^/mg, "\t") + "\n" } } if (block0.stack) syntax += "end" let index = block.parent.stack.indexOf(block) let next = block.parent.stack[index + 1] if (!next) return syntax return syntax + "\n" + toSyntax(next, options) } export function fromSyntax(syntax, options = {}) { let blocks = [] let block0 let block1 function flush() { if (block0 && block0.stack.length > 0) blocks.push(block0.stack[0]) block0 = Forever() block1 = block0 } flush() let parameters0 = {text: options.parameters?.text?.slice() ?? [], boolean: options.parameters?.boolean?.slice() ?? []} let variables = options.variables ?? [] let lists = options.lists ?? [] let custom = options.custom ?? [] let parameters = structuredClone(parameters0) let types0 = new Set(options.types) let types = new Set(types0) while (true) { let found for (let type of [...types]) { if (!type.complement) continue if (types.has(type.complement)) continue types.add(type.complement) found = true } if (!found) break } options = {...options, variables, lists, custom, parameters, types, parameters0} if (!options.dry) { if (!options.mutate) options = {...options, variables: variables.slice(), lists: lists.slice(), custom: custom.slice()} fromSyntax(syntax, {...options, dry: true}) } for (let line of syntax.normalize().split("\n")) { let infos = [{type: "block", text: "{{", children: []}] let tokens = [...line.matchAll(/\((\\.|[^\(\)\[<{])*\sv\)|\[(\\.|[^\]])*\]|::|[\(\)\[\]{}<>:]|[^\(\)\[\]{}<>:\s]+/ugd)] let comment = "" for (let i = 0 ; i < tokens.length ; i++) { let [token] = tokens[i] let {indices: [[index]]} = tokens[i] if (token.match(/^\[.*\sv\]|\(.*\sv\)$/u)) { let text = unescape(token.slice(1, -2).split(/\s+/ug).join(" ")) infos[0].children.push({type: "slot", text}) continue } if (token.match(/^\[.*\]$/u)) { let text = unescape(token.slice(1, -1)) infos[0].children.push({type: "slot", text}) continue } if (token === "(") { if ((tokens[i + 1]?.[0] ?? ")") === ")") { infos[0].children.push({type: "slot", text: ""}) i++ continue } let text = unescape(tokens[i + 1]?.[0]) if ((tokens[i + 2]?.[0] ?? ")") === ")" && !Number.isNaN(Number(text))) { infos[0].children.push({type: "slot", text}) i += 2 continue } } if (token === "::") { infos[0].children.push({type: "end"}) continue } if (infos[0].text === "{{" && token.includes("//")) { let j = token.indexOf("//") token = token.slice(0, j) tokens.splice(i) comment = line.slice(index + j) if (!token) continue } if (token === "<" && (tokens[i + 1]?.[0] ?? ">") === ">") { infos[0].children.push({type: "slot", text: "", boolean: true}) i++ continue } let ltgt = (token === "<" || token === ">") && infos[0].children.length === 1 && typeof infos[0].children[0] !== "string" && tokens[i + 1]?.[0][0] in braces if (token in braces && !ltgt) { let next = {type: "block", text: token, children: []} infos[0].children.push(next) infos.unshift(next) continue } if (infos[0].text in braces && token === braces[infos[0].text] && !ltgt) { infos.shift() continue } let children = infos[0].children if (typeof children[children.length - 1] !== "string") infos[0].children.push(unescape(token)) else children[children.length - 1] += " " + unescape(token) } let info = infos[infos.length - 1] if (info.children.length === 0) { if (block1 === block0 || block1.type.hat) flush() continue } if (info.children.length === 1 && info.children[0] === "end" && block1.stack) { while (block1.parent?.complement === block1) block1 = block1.parent block1 = block1.parent ?? block1 continue } let block = match(info, options) if (!block) continue if (!types0.has(block.type) && block.type.category !== "custom") continue if (block.type.shape === "slot") continue if (options.dry) continue if (block.type.shape !== undefined) { flush() blocks.push(block) continue } if (block.type === Define.type && comment.match(/\batomic\b/)) { block.atomic = true } if (block.type.hat) { blocks.push(block) block1 = block continue } if (block.type === block1.type.complement) { complement(block1, block) block1 = block continue } if (block1.stack[block1.stack.length - 1]?.type.cap) continue append(block1, block) if (block.stack) block1 = block } flush() return blocks } function match(info, options) { let extras = [] for (;;) { while (info.children.length === 1 && typeof info.children[0] !== "string" && info.children[0].type === "block") { info = info.children[0] } let index = info.children.findIndex(a => a.type === "end") if (index < 0) break extras.push(...info.children.splice(index).slice(1)) } if (info.children.length === 1) { let name0 = info.children[0] if (typeof name0 === "string" && (info.text === "(" || info.text === "<")) { let name = toName(name0) if (extras.includes("variables") || options.variables.includes(name)) { return options.dry || Variable(name) } if (extras.includes("lists") || options.lists.includes(name)) { return options.dry || List(name) } let type = info.text === "<" ? "boolean" : "text" if (extras.includes("custom") && info.text !== "{" || options.parameters[type].includes(name)) { if (options.dry) return return type === "boolean" ? BooleanParameter(name) : TextParameter(name) } } } let match if (info.text === "{{" && !extras.includes("stack")) match = info.children.map((part, i) => typeof part === "string" ? toName(part) : `((${i}))`).filter(Boolean).join(" ").match(toDefineRegex(options)) if (match) { info.children = match[1].split(/(\(\([0-9]+\)\))/g).map(part => info.children[part.match(/^\(\(([0-9]+)\)\)$/)?.[1] ?? -1] ?? toName(part)) let name = [] for (let child of info.children) { if (typeof child === "string") { name.push(child) continue } if (child.type !== "block") continue if (child.children.length !== 1) continue if (typeof child.children[0] !== "string") continue let parameter = {type: child.text === "<" ? "boolean" : "text", name: child.children[0]} name.push(parameter) options.parameters[parameter.type].push(parameter.name) } if (name.length !== 0) { if (!options.custom.some(name0 => compareName(name0, name))) options.custom.push(name) if (!options.dry) return Define(name) } return } if (extras.includes("custom")) return matchCustom(info, options, true) let matches = [] for (let type of options.types) { let inputs = [...type.references ?? [], ...type.slots ?? []].map((_a, i) => i) let name = options.getName?.(type, ...inputs) ?? type.name?.(...inputs) if (!name) continue let array = name.map(parts0 => { let parts1 = [] if (!(parts0 instanceof Array)) parts0 = [parts0] for (let part of parts0) { if (part instanceof Element) continue if (typeof part === "string") { parts1.push(part) continue } if (typeof part === "number") { parts1.push(part) continue } if (part.options) { parts1.push({options: part.options}) continue } parts1.push("") } if (parts1.length === 0) parts1.push("") return parts1 }) let names = [[]] for (let parts of array) { for (let name of names.splice(0)) { for (let part of parts) { names.push([...name, part]) } } } for (let name of names) { let matched = true let perfectMatch = true for (let part of name.splice(0)) { if (part === "") continue if (typeof part === "string" && typeof name[name.length - 1] === "string") { name[name.length - 1] += " " + part continue } name.push(part) } if (info.children.length !== name.length) continue let inputs = [] let names = type.references?.map(() => "") ?? [] for (let [i, part0] of info.children.entries()) { if (typeof part0.type !== "string") { while (part0.type === "block" && part0.children.length === 1) { if (typeof part0.children[0] === "string") break part0 = part0.children[0] } } let part1 = name[i] if ((typeof part0 === "string") !== (typeof part1 === "string")) { matched = false break } if (typeof part0 === "string") { if (part0 === part1) continue matched = false break } if (typeof part1 === "string") { matched = false break } if (typeof part1 !== "number") { if (part0.type === "slot" && part1.options) { let name = toName(part0.text) let found for (let option of part1.options) { if (option.label === name) { type = option.type found = true break } } if (found) continue } matched = false break } if (type.references && part1 < type.references.length) { let reference = type.references?.[part1] if (reference !== "variable" && reference !== "list") { matched = false break } if (part0.type !== "slot") { matched = false break } let name0 = toName(part0.text) if (!options[reference + "s"].includes(name0)) options[reference].push(name0) names[part1] = name0 continue } part1 -= type.references?.length ?? 0 let Slot = type.slots[part1] if (!Slot) { matched = false break } if (part0.type !== "slot") { inputs[part1] = () => match(part0, options) continue } let value = toName(part0.text) for (let option of Slot.type.options ?? []) { let name = options.getName?.(Slot.type, option) ?? Slot.type.name?.(option) ?? option if (name === value) { value = option break } } if (Slot.type.dynamic && !Slot.type.options.includes(value)) Slot.type.options.push(value) inputs[part1] = () => Slot(value) } if (!matched) continue if (perfectMatch) { matches = [{type, inputs, names}] break } matches.push({type, inputs, names}) } } if (!matches[0]) { if (info.children.length === 1) { let name0 = info.children[0] if (typeof name0 === "string" && info.text === "(") { let name = toName(name0) if (!options.variables.includes(name)) options.variables.push(name) return options.dry || Variable(name) } } return matchCustom(info, options) } if (options.dry) return let {type, inputs, names} = matches[0] let block = MakeBlock(type)() if (block.type.hat) Object.assign(options.parameters, structuredClone(options.parameters0)) for (let [i, make] of inputs.entries()) { let input = make() ?? block.type.slots[i]() if (input.type.shape !== "slot") { if (input.type.shape !== "reporter") { input = block.type.slots[i]() } if (block.type.slots[i].type.canFit && !block.type.slots[i].type.canFit(input)) { input = block.type.slots[i]() } } replace(block.inputs[i], input) } for (let i of names.keys()) block.names[i] = names[i] return block } function matchCustom(info, options, force) { let matches = [] for (let name of options.custom) { let perfectMatch = true let matched = true for (let [i, part] of name.entries()) { let part1 = info.children[i] if ((typeof part === "string") !== (typeof part1 === "string")) { matched = false break } if (typeof part === "string") { if (part !== part1) { matched = false break } continue } if (part.type !== "boolean") { if (part1.type === "block" && part1.text === "<") perfectMatch = false if (part1.type === "slot" && part1.boolean) perfectMatch = false continue } if (part1.type === "block" && part1.text !== "<") { matched = false break } if (part1.type === "slot" && !part1.boolean) { matched = false break } } if (!matched) continue if (perfectMatch) { matches = [name] break } matches.push(name) } if (!matches[0]) { if (!force) return let name = [] matches = [name] options.custom.push(name) let n = 1 for (let part of info.children) { if (typeof part === "string") { name.push(part) continue } let parameter = {name: `input ${n++}`, type: "text"} name.push(parameter) if (part.type !== "slot" ? part.text === "<" : part.boolean) { parameter.type = "boolean" } } } if (options.dry) return let inputs = [] for (let [i, part] of matches[0].entries()) { if (typeof part === "string") continue let part0 = info.children[i] while (part0.type === "block" && part0.children.length === 1) { if (typeof part0.children[0] === "string") break part0 = part0.children[0] } if (part0.type === "slot") { if (part.type === "boolean") inputs.push(BooleanSlot()) else inputs.push(TextSlot(part0.text)) continue } let input = match(part0, options) if (input.type.shape !== "reporter") { inputs.push(input) continue } if (part.type === "boolean") inputs.push(BooleanSlot()) else inputs.push(TextSlot()) } return Custom(matches[0], ...inputs) }