UNPKG

scratchblocks

Version:

Make pictures of Scratch blocks from text.

386 lines (342 loc) 8.95 kB
function assert(bool, message) { if (!bool) { throw new Error(`Assertion failed! ${message || ""}`) } } function indent(text) { return text .split("\n") .map(line => { return ` ${line}` }) .join("\n") } import { parseSpec, inputPat, parseInputNumber, iconPat, rtlLanguages, unicodeIcons, } from "./blocks.js" export class Label { constructor(value, cls) { this.value = value this.cls = cls || "" this.el = null this.height = 12 this.metrics = null this.x = 0 } get isLabel() { return true } stringify() { if (this.value === "<" || this.value === ">") { return this.value } return this.value.replace(/([<>[\](){}])/g, "\\$1") } } export class Icon { constructor(name) { this.name = name this.isArrow = name === "loopArrow" assert(Icon.icons[name], `no info for icon ${name}`) } get isIcon() { return true } static get icons() { return { greenFlag: true, stopSign: true, turnLeft: true, turnRight: true, loopArrow: true, addInput: true, delInput: true, list: true, } } stringify() { return unicodeIcons[`@${this.name}`] || "" } } export class Input { constructor(shape, value, menu) { this.shape = shape this.value = value this.menu = menu || null this.isRound = shape === "number" || shape === "number-dropdown" this.isBoolean = shape === "boolean" this.isStack = shape === "stack" this.isInset = shape === "boolean" || shape === "stack" || shape === "reporter" this.isColor = shape === "color" this.hasArrow = shape === "dropdown" || shape === "number-dropdown" this.isDarker = shape === "boolean" || shape === "stack" || shape === "dropdown" this.isSquare = shape === "string" || shape === "color" || shape === "dropdown" this.hasLabel = !(this.isColor || this.isInset) this.label = this.hasLabel ? new Label(value, `literal-${this.shape}`) : null this.x = 0 } get isInput() { return true } stringify() { if (this.isColor) { assert(this.value[0] === "#") return `[${this.value}]` } // Order sensitive; see #439 let text = (this.value ? String(this.value) : "") .replace(/([\]\\])/g, "\\$1") .replace(/ v$/, " \\v") if (this.hasArrow) { text += " v" } return this.isRound ? `(${text})` : this.isSquare ? `[${text}]` : this.isBoolean ? "<>" : this.isStack ? "{}" : text } translate(_lang) { if (this.hasArrow) { const value = this.menu || this.value this.value = value // TODO translate dropdown value this.label = new Label(this.value, `literal-${this.shape}`) } } } export class Block { constructor(info, children, comment) { assert(info) this.info = { ...info } this.children = children this.comment = comment || null this.diff = null const shape = this.info.shape this.isHat = shape === "hat" || shape === "cat" || shape === "define-hat" this.hasPuzzle = shape === "stack" || shape === "hat" || shape === "cat" || shape === "c-block" this.isFinal = /cap/.test(shape) this.isCommand = shape === "stack" || shape === "cap" || /block/.test(shape) this.isOutline = shape === "outline" this.isReporter = shape === "reporter" this.isBoolean = shape === "boolean" this.isRing = shape === "ring" this.hasScript = /block/.test(shape) this.isElse = shape === "celse" this.isEnd = shape === "cend" } get isBlock() { return true } stringify(extras) { let firstInput = null let checkAlias = false let text = this.children .map(child => { if (child.isIcon) { checkAlias = true } if (!firstInput && !(child.isLabel || child.isIcon)) { firstInput = child } return child.isScript ? `\n${indent(child.stringify())}\n` : child.stringify().trim() + " " }) .join("") .trim() const lang = this.info.language if (checkAlias && lang && this.info.selector) { const aliases = lang.nativeAliases[this.info.id] if (aliases && aliases.length) { let alias = aliases[0] // TODO make translate() not in-place, and use that if (inputPat.test(alias) && firstInput) { alias = alias.replace(inputPat, firstInput.stringify()) } return alias } } let overrides = extras || "" if ( this.info.categoryIsDefault === false || (this.info.category === "custom-arg" && (this.isReporter || this.isBoolean)) || (this.info.category === "custom" && this.info.shape === "stack") ) { if (overrides) { overrides += " " } overrides += this.info.category } if (overrides) { text += ` :: ${overrides}` } return this.hasScript ? text + "\nend" : this.info.shape === "reporter" ? `(${text})` : this.info.shape === "boolean" ? `<${text}>` : text } translate(lang, isShallow) { if (!lang) { throw new Error("Missing language") } const id = this.info.id if (!id) { return } if (id === "PROCEDURES_DEFINITION") { // Find the first 'outline' child (there should be exactly one). const outline = this.children.find(child => child.isOutline) this.children = [] for (const word of lang.definePrefix) { this.children.push(new Label(word)) } this.children.push(outline) for (const word of lang.defineSuffix) { this.children.push(new Label(word)) } return } const oldSpec = this.info.language.commands[id] const nativeSpec = lang.commands[id] if (!nativeSpec) { return } const nativeInfo = parseSpec(nativeSpec) const rawArgs = this.children.filter( child => !child.isLabel && !child.isIcon, ) if (!isShallow) { rawArgs.forEach(child => child.translate(lang)) } // Work out indexes of existing children const oldParts = parseSpec(oldSpec).parts const oldInputOrder = oldParts .map(part => parseInputNumber(part)) .filter(x => x) let highestNumber = 0 const args = oldInputOrder.map(number => { highestNumber = Math.max(highestNumber, number) return rawArgs[number - 1] }) const remainingArgs = rawArgs.slice(highestNumber) // Get new children by index this.children = nativeInfo.parts .map(part => { part = part.trim() if (!part) { return } const number = parseInputNumber(part) if (number) { return args[number - 1] } return iconPat.test(part) ? new Icon(part.slice(1)) : new Label(part) }) .filter(x => x) // Push any remaining children, so we pick up C block bodies remainingArgs.forEach((arg, index) => { if (index === 1 && this.info.id === "CONTROL_IF") { this.children.push(new Label(lang.commands.CONTROL_ELSE)) } this.children.push(arg) }) this.info.language = lang this.info.isRTL = rtlLanguages.includes(lang.code) this.info.categoryIsDefault = true } } export class Comment { constructor(value, hasBlock) { this.label = new Label(value, "comment-label") this.width = null this.hasBlock = hasBlock } get isComment() { return true } stringify() { return `// ${this.label.value}` } } export class Glow { constructor(child) { assert(child) this.child = child if (child.isBlock) { this.shape = child.info.shape this.info = child.info } else { this.shape = "stack" } } get isGlow() { return true } stringify() { if (this.child.isBlock) { return this.child.stringify("+") } const lines = this.child.stringify().split("\n") return lines.map(line => `+ ${line}`).join("\n") } translate(lang) { this.child.translate(lang) } } export class Script { constructor(blocks) { this.blocks = blocks this.isEmpty = !blocks.length this.isFinal = !this.isEmpty && blocks[blocks.length - 1].isFinal } get isScript() { return true } stringify() { return this.blocks .map(block => { let line = block.stringify() if (block.comment) { line += ` ${block.comment.stringify()}` } return line }) .join("\n") } translate(lang) { this.blocks.forEach(block => block.translate(lang)) } } export class Document { constructor(scripts) { this.scripts = scripts } stringify() { return this.scripts.map(script => script.stringify()).join("\n\n") } translate(lang) { this.scripts.forEach(script => script.translate(lang)) } }