ref: 81d1e4e01d7001e6a0107792e6167136787295d6
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)
}