From fc9e2896301f0fa29150c269c0ed7f2d0c95b26d Mon Sep 17 00:00:00 2001 From: "Tate, Hongliang Tian" <tatetian@gmail.com> Date: Tue, 3 Mar 2015 17:21:40 +0800 Subject: [PATCH] Refactor the project to node.js --- Makefile | 38 + PseudoCode.js | 1366 +------------------ package.json | 28 + src/Lexer.js | 128 ++ src/ParseError.js | 21 + src/Parser.js | 458 +++++++ src/Renderer.js | 752 ++++++++++ src/utils.js | 22 + css/PseudoCode.css => static/pseudocode.css | 0 test-suite.html => static/test-suite.html | 14 +- 10 files changed, 1465 insertions(+), 1362 deletions(-) create mode 100644 Makefile create mode 100644 package.json create mode 100644 src/Lexer.js create mode 100644 src/ParseError.js create mode 100644 src/Parser.js create mode 100644 src/Renderer.js create mode 100644 src/utils.js rename css/PseudoCode.css => static/pseudocode.css (100%) rename test-suite.html => static/test-suite.html (90%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d2649fd --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +.PHONY: setup lint build zip build/pseudocode clean + +build: lint build/pseudocode.min.js build/pseudocode.min.css + +setup: + npm install + mkdir -p build + +# watch the changes to js source code and update the target js code +watch-js: pseudocode.js $(wildcard src/*.js) + ./node_modules/.bin/watchify $< --standalone pseudocode -o build/pseudocode.js + +clean: + rm -rf build/* + +zip: build/pseudocode-js.tar.gz build/pseudocode-js.zip + +lint: pseudocode.js $(wildcard src/*.js) + ./node_modules/.bin/jshint $^ + +build/pseudocode.js: pseudocode.js $(wildcard src/*.js) + ./node_modules/.bin/browserify $< --standalone pseudocode -o $@ + +build/pseudocode.min.js: build/pseudocode.js + ./node_modules/.bin/uglifyjs --mangle --beautify beautify=false < $< > $@ + +build/pseudocode.min.css: static/pseudocode.css + ./node_modules/.bin/cleancss -o $@ $< + +build/pseudocode: build/pseudocode.min.js build/pseudocode.min.css README.md + mkdir -p build/pseudocode + cp -r $^ build/pseudocode + +build/pseudocode-js.tar.gz: build/pseudocode + cd build && tar czf pseudocode-js.tar.gz pseudocode/ + +build/pseudocode-js.zip: build/pseudocode + cd build && zip -rq pseudocode-js.zip pseudocode/ diff --git a/PseudoCode.js b/PseudoCode.js index b480adf..7ab5fbf 100644 --- a/PseudoCode.js +++ b/PseudoCode.js @@ -1,1356 +1,13 @@ -/* -The TeX-style pseudocode language (follows **algoritmic** environment) -represented in a context-free grammar: +var ParseError = require('./src/ParseError'); +var Lexer = require('./src/Lexer'); +var Parser = require('./src/Parser'); +var Renderer = require('./src/Renderer'); - <pseudo> :== ( <algorithm> | <algorithmic> )[0..n] - - <algorithm> :== \begin{algorithm} - + ( <caption> | <algorithmic> )[0..n] - \end{algorithm} - <caption> :== \caption{ <close-text> } - - <algorithmic> :== \begin{algorithmic} - + ( <ensure> | <require> | <block> )[0..n] - + \end{algorithmic} - <require> :== \REQUIRE + <open-text> - <ensure> :== \ENSURE + <open-text> - - <block> :== ( <control> | <function> - | <statement> | <comment> | <call> )[0..n] - - <control> :== <if> | <for> | <while> - <if> :== \IF{<cond>} + <block> - + ( \ELIF{<cond>} <block> )[0..n] - + ( \ELSE <block> )[0..1] - + \ENDIF - <for> :== \FOR{<cond>} + <block> + \ENDFOR - <while> :== \WHILE{<cond>} + <block> + \ENDWHILE - - <function> :== \FUNCTION{<name>}{<params>} <block> \ENDFUNCTION - (same for <procedure>) - - <statement> :== <state> | <return> | <print> - <state> :== \STATE + <open-text> - <return> :== \RETURN + <open-text> - <print> :== \PRINT + <open-text> - - <comment> :== \COMMENT{<close-text>} - - <call> :== \CALL{<name>}({<close-text>})[0..1] - - <cond> :== <close-text> - <open-text> :== <atom> + <open-text> | { <close-text> } | <empty> - <close-text> :== <atom> + <close-text> | { <close-text> } | <empty> - - <atom> :== <ordinary>[1..n] | <special> | <symbol> - | <size> | <font> | <bool> | <math> - <name> :== <ordinary> - - <special> :== \\ | \{ | \} | \$ | \& | \# | \% | \_ - <cond-symbol> :== \AND | \OR | \NOT | \TRUE | \FALSE | \TO - <text-symbol> :== \textbackslash - (More LaTeX symbols can be added if necessary. See - http://get-software.net/info/symbols/comprehensive/symbols-a4.pdf.) - <math> :== \( + ... + \) | $ ... $ - (Math are handled by KaTeX) - <size> :== \tiny | \scriptsize | \footnotesize | \small - | \normalsize | \large | \Large | \LARGE | \huge - | \HUGE - <font> :== \rmfamily | \sffamily | \ttfamily - | \upshape | \itshape | \slshape | \scshape - <ordinary> :== not any of \ { } $ & # % _ - <empty> :== - -There are many well-known ways to parse a context-free grammar, like the -top-down approach LL(k) or the bottom-up approach like LR(k). Both methods are -usually implemented in a table-driven fashion, which is not suitable to write -by hand. As our grammar is simple enough and its input is not expected to be -large, the performance wouldn't be a problem. Thus, I choose to write the parser -in the most natural form--- a (predictive) recursive descent parser. The major benefit of a -recursive descent parser is **simplity** for the structure of resulting program -closely mirrors that of the grammar. - -TODO: - * comment - * color{#FF0000}{text} - * line number every k lines: \begin{algorithmic}[k] - * caption without the number: \caption*{} - * rename: e.g. require --> input, ensure --> output - * elimiate the default space (smaller than a ' ' char) between spans - * double quotes -*/ - -(function(parentModule, katex) { // rely on KaTex to process TeX math - -// =========================================================================== -// Utility functions -// =========================================================================== - -function isString(str) { - return (typeof str === 'string') || (str instanceof String); -} - -function isObject(obj) { - return (typeof obj === 'object' && (obj instanceof Object)); -} - -function toString(obj) { - if (!isObject(obj)) return obj + ''; - - var parts = []; - for (var member in obj) - parts.push(member + ': ' + toString(obj[member])); - return parts.join(', '); -} - -var entityMap = { - "&": "&", - "<": "<", - ">": ">", - '"': '"', - "'": ''', - "/": '/' - }; - -function escapeHtml(string) { - return String(string).replace(/[&<>"'\/]/g, function (s) { - return entityMap[s]; - }); - } - -// =========================================================================== -// Error handling -// =========================================================================== - -function ParseError(message, pos, input) { - var error = 'Error: ' + message; - // If we have the input and a position, make the error a bit fancier - if (pos !== undefined && input !== undefined) { - error += " at position " + pos + ": `"; - - // Insert a combining underscore at the correct position - input = input.slice(0, pos) + "\u21B1" + input.slice(pos); - - // Extract some context from the input and add it to the error - var begin = Math.max(0, pos - 15); - var end = pos + 15; - error += input.slice(begin, end) + "`"; - } - - this.message = error; -}; -ParseError.prototype = Object.create(Error.prototype); -ParseError.prototype.constructor = ParseError; - -// =========================================================================== -// Lexer -// =========================================================================== - -/* Math pattern - Math environtment like $ $ or \( \) cannot be matched using regular - expression. This object simulates a regular expression*/ -var mathPattern = { - exec: function(str) { - if (str.indexOf('$') != 0) return null; - - var pos = 1; - var len = str.length; - while (pos < len && ( str[pos] != '$' || str[pos - 1] == '\\' ) ) pos++; - - if (pos === len) return null; - return [str.substring(0, pos + 1), str.substring(1, pos)]; - } -}; -var atomRegex = { - // TODO: which is correct? func: /^\\(?:[a-zA-Z]+|.)/, - special: /^(\\\\|\\{|\\}|\\\$|\\&|\\#|\\%|\\_)/, - func: /^\\([a-zA-Z]+)/, - open: /^\{/, - close: /^\}/, - ordinary: /^[^\\{}$&#%_\s]+/, - math: mathPattern ///^\$.*\$/ -}; -var whitespaceRegex = /^\s*/; - -var Lexer = function(input) { - this._input = input; - this._remain = input; - this._pos = 0; - this._nextAtom = this._currentAtom = null; - this.next(); // get the next atom -}; - -Lexer.prototype.accept = function(type, text) { - if (this._nextAtom.type === type && this._matchText(text)) { - this.next(); - return this._currentAtom.text; - } - return null; -}; - -Lexer.prototype.expect = function(type, text) { - var nextAtom = this._nextAtom; - // The next atom is NOT of the right type - if (nextAtom.type !== type) - throw new ParseError('Expect an atom of ' + type + ' but received ' + - nextAtom.type, this._pos, this._input); - // Check whether the text is exactly the same - if (!this._matchText(text)) - throw new ParseError('Expect `' + text + '` but received `' + - nextAtom.text + '`', this._pos, this._input); - - this.next(); - return this._currentAtom.text; -}; - -Lexer.prototype.get = function() { - return this._currentAtom; -}; - -/* Get the next atom */ -Lexer.prototype.next = function() { - // Skip whitespace (zero or more) - var whitespaceLen = whitespaceRegex.exec(this._remain)[0].length; - this._pos += whitespaceLen; - this._remain = this._remain.slice(whitespaceLen); - - // Remember the current atom - this._currentAtom = this._nextAtom; - - // Reach the end of string - if (this._remain === '') { - this._nextAtom = { - type: 'EOF', - text: null, - whitespace: false - }; - return false; - } - - // Try all kinds of atoms - for (var type in atomRegex) { - var regex = atomRegex[type]; - - var match = regex.exec(this._remain); - if (!match) continue; // not matched - - // match[1] is the useful part, e.g. '123' of '$123$', 'it' of '\\it' - var matchText = match[0]; - var usefulText = match[1] ? match[1] : matchText; - - this._nextAtom = { - type: type, /* special, func, open, close, ordinary, math */ - text: usefulText, /* the text value of the atom */ - whitespace: whitespaceLen > 0 /* any whitespace before the atom */ - }; - console.log('type: ' + type + ', text: ' + usefulText); - - this._pos += matchText.length; - this._remain = this._remain.slice(match[0].length); - - return true; - } - - throw new ParseError('Unrecoganizable atom', this._pos, this._input); -}; - -/* Check whether the text of the next atom matches */ -Lexer.prototype._matchText = function(text) { - // don't need to match - if (text === undefined) return true; - - if (isString(text)) // is a string, exactly the same? - return text === this._nextAtom.text; - else // is a list, match any of them? - return text.indexOf(this._nextAtom.text) >= 0; -}; - -// =========================================================================== -// Parser -// =========================================================================== - -var ParseNode = function(type, val) { - this.type = type; - this.value = val; - this.children = []; -}; - -ParseNode.prototype.toString = function(level) { - if (!level) level = 0; - - var indent = ''; - for (var i = 0; i < level; i++) indent += ' '; - - var res = indent + '<' + this.type + '>'; - if (this.value) res += ' (' + toString(this.value) + ')'; - res += '\n'; - - if (this.children) { - for (var ci = 0; ci < this.children.length; ci++) { - var child = this.children[ci]; - res += child.toString(level + 1); - } - } - - return res; -} - -ParseNode.prototype.addChild = function(childNode) { - if (!childNode) throw 'argument cannot be null'; - this.children.push(childNode); -}; - -/* AtomNode is the leaf node of parse tree */ -var AtomNode = function(type, value, whitespace) { - // ParseNode.call(this, type, val); - this.type = type; - this.value = value; - this.children = null; // leaf node, thus no children - this.whitespace = !!whitespace; // is there any whitespace before the atom -} -AtomNode.prototype = ParseNode.prototype; - -var Parser = function(lexer) { - this._lexer = lexer; -}; - -Parser.prototype.parse = function() { - var root = new ParseNode('root'); - - while (true) { - var envName = this._acceptEnvironment(); - if (envName === null) break; - - var envNode; - if (envName === 'algorithm') - envNode = this._parseAlgorithmInner(); - else if (envName === 'algorithmic') - envNode = this._parseAlgorithmicInner(); - else - throw new ParseError('Unexpected environment ' + envName); - - this._closeEnvironment(envName); - root.addChild(envNode); - } - this._lexer.expect('EOF'); - return root; -}; - -Parser.prototype._acceptEnvironment = function() { - var lexer = this._lexer; - // \begin{XXXXX} - if (!lexer.accept('func', 'begin')) return null; - - lexer.expect('open'); - var envName = lexer.expect('ordinary'); - lexer.expect('close'); - return envName; -} - -Parser.prototype._closeEnvironment = function(envName) { - // \close{XXXXX} - var lexer = this._lexer; - lexer.expect('func', 'end'); - lexer.expect('open'); - lexer.expect('ordinary', envName); - lexer.expect('close'); -} - -Parser.prototype._parseAlgorithmInner = function() { - var algNode = new ParseNode('algorithm'); - while (true) { - var envName = this._acceptEnvironment(); - if (envName !== null) { - if (envName !== 'algorithmic') - throw new ParseError('Unexpected environment ' + envName); - var algmicNode = this._parseAlgorithmicInner(); - this._closeEnvironment(); - algNode.addChild(algmicNode); - continue; - } - - var captionNode = this._parseCaption(); - if (captionNode) { - algNode.addChild(captionNode); - continue; - } - - break; - } - return algNode; -} - -Parser.prototype._parseAlgorithmicInner = function() { - var algmicNode = new ParseNode('algorithmic'); - while (true) { - var node; - if (!(node = this._parseCommand(CONDITION_COMMANDS)) && - !((node = this._parseBlock()).children.length > 0)) break; - - algmicNode.addChild(node); - } - return algmicNode; -}; - -Parser.prototype._parseCaption = function() { - var lexer = this._lexer; - if (!lexer.accept('func', 'caption')) return null; - - var captionNode = new ParseNode('caption'); - lexer.expect('open'); - captionNode.addChild(this._parseCloseText()); - lexer.expect('close'); - - return captionNode; -} - -Parser.prototype._parseBlock = function() { - var blockNode = new ParseNode('block'); - - while (true) { - var controlNode = this._parseControl(); - if (controlNode) { blockNode.addChild(controlNode); continue; } - - var functionNode = this._parseFunction(); - if (functionNode) { blockNode.addChild(functionNode); continue; } - - var commandNode = this._parseCommand(STATEMENT_COMMANDS); - if (commandNode) { blockNode.addChild(commandNode); continue; } - - var commentNode = this._parseComment(); - if (commentNode) { blockNode.addChild(commentNode); continue; } - - var callNode = this._parseCall(); - if (callNode) { blockNode.addChild(callNode); continue; } - - break; - } - - return blockNode; -}; - -Parser.prototype._parseControl = function() { - var controlNode; - if ((controlNode = this._parseIf())) return controlNode; - if ((controlNode = this._parseLoop())) return controlNode; -}; - -Parser.prototype._parseFunction = function() { - var lexer = this._lexer; - if (!lexer.accept('func', ['FUNCTION', 'PROCEDURE'])) return null; - - // \FUNCTION{funcName}{funcArgs} - var funcType = this._lexer.get().text; // FUNCTION or PROCEDURE - lexer.expect('open'); - var funcName = lexer.expect('ordinary'); - lexer.expect('close'); - lexer.expect('open'); - var argsNode = this._parseCloseText(); - lexer.expect('close'); - // <block> - var blockNode = this._parseBlock(); - // \ENDFUNCTION - lexer.expect('func', 'END' + funcType); - - var functionNode = new ParseNode('function', - {type: funcType, name: funcName}); - functionNode.addChild(argsNode); - functionNode.addChild(blockNode); - return functionNode; -} - -Parser.prototype._parseIf = function() { - if (!this._lexer.accept('func', 'IF')) return null; - - var ifNode = new ParseNode('if'); - - // { <cond> } <block> - this._lexer.expect('open'); - ifNode.addChild(this._parseCond()); - this._lexer.expect('close'); - ifNode.addChild(this._parseBlock()); - - // ( \ELIF { <cond> } <block> )[0...n] - var numElif = 0; - while (this._lexer.accept('func', 'ELIF')) { - this._lexer.expect('open'); - ifNode.addChild(this._parseCond()); - this._lexer.expect('close'); - ifNode.addChild(this._parseBlock()); - numElif++; - } - - // ( \ELSE <block> )[0..1] - var hasElse = false; - if (this._lexer.accept('func', 'ELSE')) { - hasElse = true; - ifNode.addChild(this._parseBlock()); - } - - // \ENDIF - this._lexer.expect('func', 'ENDIF'); - - ifNode.value = {numElif: numElif, hasElse: hasElse}; - return ifNode; -}; - -Parser.prototype._parseLoop = function() { - if (!this._lexer.accept('func', ['FOR', 'FORALL', 'WHILE'])) return null; - - var loopName = this._lexer.get().text; - var loopNode = new ParseNode('loop', loopName); - - // { <cond> } <block> - this._lexer.expect('open'); - loopNode.addChild(this._parseCond()); - this._lexer.expect('close'); - loopNode.addChild(this._parseBlock()); - - // \ENDFOR - var endLoop = loopName !== 'FORALL' ? 'END' + loopName : 'ENDFOR'; - this._lexer.expect('func', endLoop); - - return loopNode; -}; - -var CONDITION_COMMANDS = ['ENSURE', 'REQUIRE']; -var STATEMENT_COMMANDS = ['STATE', 'PRINT', 'RETURN']; -Parser.prototype._parseCommand = function(acceptCommands) { - if (!this._lexer.accept('func', acceptCommands)) return null; - - var cmdName = this._lexer.get().text; - var cmdNode = new ParseNode('command', cmdName); - cmdNode.addChild(this._parseOpenText()); - return cmdNode; -}; - -Parser.prototype._parseComment = function() { - if (!this._lexer.accept('func', 'COMMENT')) return null; - - var commentNode = new ParseNode('comment'); - - // { \text } - this._lexer.expect('open'); - commentNode.addChild(this._parseCloseText()); - this._lexer.expect('close'); - - return commentNode; -}; - -Parser.prototype._parseCall = function() { - var lexer = this._lexer; - if (!lexer.accept('func', 'CALL')) return null; - - var anyWhitespace = lexer.get().whitespace; - - // \CALL { <ordinary> } ({ <text> })[0..1] - lexer.expect('open'); - var funcName = lexer.expect('ordinary'); - lexer.expect('close'); - - var callNode = new ParseNode('call'); - callNode.whitespace = anyWhitespace; - callNode.value = funcName; - - lexer.expect('open'); - var argsNode = this._parseCloseText(); - callNode.addChild(argsNode); - lexer.expect('close'); - return callNode; -}; - -Parser.prototype._parseCond = -Parser.prototype._parseCloseText = function() { - return this._parseText('close'); -}; -Parser.prototype._parseOpenText = function() { - return this._parseText('open'); -}; - -Parser.prototype._parseText = function(openOrClose) { - var textNode = new ParseNode(openOrClose + '-text'); - - var atomNode; - while (true) { - atomNode = this._parseAtom(); - if (atomNode) { - textNode.addChild(atomNode); - continue; - } - - if (this._lexer.accept('open')) { - var subTextNode = this._parseCloseText(); - textNode.addChild(subTextNode); - this._lexer.expect('close'); - continue; - } - - break; - } - - return textNode; -}; - -Parser.prototype._parseAtom = function() { - var atom; - - var text; - if (text = this._lexer.accept('ordinary')) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('ordinary', text, whitespace); - } - else if (text = this._lexer.accept('math')) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('math', text, whitespace); - } - else if (text = this._lexer.accept('special')) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('special', text, whitespace); - } - else if (text = this._lexer.accept('func', - ['AND', 'OR', 'NOT', 'TRUE', 'FALSE', 'TO'])) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('cond-symbol', text, whitespace); - } - else if (text = this._lexer.accept('func', - ['tiny', 'scriptsize', 'footnotesize', 'small', 'normalsize', - 'large', 'Large', 'LARGE', 'huge', 'Huge'])) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('sizing-dclr', text, whitespace); - } - else if (text = this._lexer.accept('func', - ['normalfont', 'rmfamily', 'sffamily', 'ttfamily', - 'upshape', 'itshape', 'slshape', 'scshape', - 'bfseries', 'mdseries', 'lfseries'])) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('font-dclr', text, whitespace); - } - else if (text = this._lexer.accept('func', - ['textnormal', 'textrm', 'textsf', 'texttt', 'textup', 'textit', - 'textsl', 'textsc', 'uppercase', 'lowercase', 'textbf', 'textmd', - 'textlf'])) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('font-cmd', text, whitespace); - } - else if (text = this._lexer.accept('func', - ['textbackslash'])) { - var whitespace = this._lexer.get().whitespace; - return new AtomNode('text-symbol', text, whitespace); - } - return null; -} - -// =========================================================================== -// Builder - Maps a ParseTree to its HTML couterpart -// The builder make use of KaTeX to render mathematical expressions. -// =========================================================================== - -function RendererOptions(options) { - options = options || {}; - this.indentSize = options.indentSize ? - this._parseEmVal(options.indentSize) : 1.2; - this.commentSymbol = options.commentSymbol || ' // '; - this.lineNumberPunc = options.lineNumberPunc || ':'; - this.lineNumber = options.lineNumber != null ? options.lineNumber : false; - this.noEnd = options.noEnd != null ? options.noEnd : false; -} - -RendererOptions.prototype._parseEmVal = function(emVal) { - var emVal = emVal.trim(); - if (emVal.indexOf('em') !== emVal.length - 2) - throw 'Option unit error; no `em` found'; - return Number(emVal.substring(0, emVal.length - 2)); -} - -/* - The font information used by builder to render the ouput HTML - - options - set attributes of font, null value means default - family - roman, sans serif, teletype - size - ..., small, normalsize, large, Large, ... - weight - normal, bold - color - - variant - none, small-caps -*/ -function TextStyle(outerFontSize) { - this._css = {}; - - this._fontSize = this._outerFontSize - = outerFontSize != null ? outerFontSize : 1.0; -} - -TextStyle.prototype.outerFontSize = function(size) { - if (size != null) this._outerFontSize = size; - return this._outerFontSize; -} - -TextStyle.prototype.fontSize = function() { - return this._fontSize; -} - -/* Update the font state by TeX command - cmd - the name of TeX command that alters current font -*/ -TextStyle.prototype._fontCommandTable = { - // -------------- declaration -------------- - // font-family - normalfont: { 'font-family': 'KaTeX_Main'}, - rmfamily: { 'font-family': 'KaTeX_Main'}, - sffamily: { 'font-family': 'KaTeX_SansSerif'}, - ttfamily: { 'font-family': 'KaTeX_Typewriter'}, - // weight - bfseries: { 'font-weight': 'bold'}, - mdseries: { 'font-weight': 'medium'}, - lfseries: { 'font-weight': 'lighter'}, - // shape - upshape: { 'font-style': 'normal', 'font-variant': 'normal'}, - itshape: { 'font-style': 'italic', 'font-variant': 'normal'}, - scshape: { 'font-style': 'normal', 'font-variant': 'small-caps'}, - slshape: { 'font-style': 'oblique', 'font-variant': 'normal'}, - // -------------- command -------------- - // font-family - textnormal: { 'font-family': 'KaTeX_Main'}, - textrm: { 'font-family': 'KaTeX_Main'}, - textsf: { 'font-family': 'KaTeX_SansSerif'}, - texttt: { 'font-family': 'KaTeX_Typewriter'}, - // weight - textbf: { 'font-weight': 'bold'}, - textmd: { 'font-weight': 'medium'}, - textlf: { 'font-weight': 'lighter'}, - // shape - textup: { 'font-style': 'normal', 'font-variant': 'normal'}, - textit: { 'font-style': 'italic', 'font-variant': 'normal'}, - textsc: { 'font-style': 'normal', 'font-variant': 'small-caps'}, - textsl: { 'font-style': 'oblique', 'font-variant': 'normal'}, - // case - uppercase: { 'text-transform': 'uppercase'}, - lowercase: { 'text-transform': 'lowercase'} -}; - -TextStyle.prototype._sizingScalesTable = { - tiny: 0.68, - scriptsize: 0.80, - footnotesize: 0.85, - small: 0.92, - normalsize: 1.00, - large: 1.17, - Large: 1.41, - LARGE: 1.58, - huge: 1.90, - Huge: 2.28 -}; - -TextStyle.prototype.updateByCommand = function(cmd) { - // Font command - var cmdStyles = this._fontCommandTable[cmd]; - if (cmdStyles !== undefined) { - for (var attr in cmdStyles) - this._css[attr] = cmdStyles[attr]; - return; - } - - // Sizing command - var fontSize = this._sizingScalesTable[cmd]; - if (fontSize !== undefined) { - this._outerFontSize = this._fontSize; - this._fontSize = fontSize; - return; - } - - throw new ParserError('unrecogniazed text-style command'); -}; - -TextStyle.prototype.toCSS = function() { - var cssStr = ''; - for (var attr in this._css) { - var val = this._css[attr]; - if (val == null) continue; - cssStr += attr + ':' + this._css[attr] + ';'; - } - if (this._fontSize !== this._outerFontSize) { - cssStr += 'font-size:' + (this._fontSize / this._outerFontSize) + 'em;'; - } - return cssStr; -}; - -function TextEnvironment(nodes, textStyle) { - this._nodes = nodes; - this._textStyle = textStyle; -} - -TextEnvironment.prototype.renderToHTML = function() { - this._html = new HTMLBuilder(); - - var node; - while (node = this._nodes.shift()) { - var type = node.type; - - // Insert whitespace before the atom if necessary - if (node.whitespace) this._html.putText(' '); - - switch(type) { - case 'ordinary': - var text = node.value; - this._html.putText(text); - break; - case 'math': - var math = node.value; - var mathHTML = katex.renderToString(math); - this._html.putSpan(mathHTML); - break; - case 'cond-symbol': - var text = node.value.toLowerCase(); - this._html.beginSpan('ps-keyword').putText(text).endSpan(); - break; - case 'special': - var escapedStr = node.value; - if (escapedStr === '\\\\') { - this._html.putHTML('<br/>'); - break; - } - var replace = { - '\\{': '{', - '\\}': '}', - '\\$': '$', - '\\&': '&', - '\\#': '#', - '\\%': '%', - '\\_': '_' - }; - var replaceStr = replace[escapedStr]; - this._html.putText(replaceStr); - break; - case 'text-symbol': - var symbolName = node.value; - var name2Values = { - 'textbackslash': '\\' - }; - var symbolValue = name2Values[symbolName]; - this._html.putText(symbolValue); - break; - case 'close-text': - var newTextStyle = new TextStyle(this._textStyle.fontSize()); - var textEnv = new TextEnvironment(node.children, newTextStyle); - this._html.putSpan(textEnv.renderToHTML()); - break; - // There are two kinds of typestyle commands: - // command (e.g. \textrm{...}). - // and - // declaration (e.g. { ... \rmfamily ... }) - // - // For typestyle commands, it works as following: - // \textsf --> create a new typestyle - // { --> save the current typestyle, and then use the new one - // ... --> the new typestyle is in use - // } --> restore the last typestyle - // - // For typestyle declaration, it works a little bit diferrently: - // { --> save the current typestyle, and then create and use - // an identical one - // ... --> the new typestyle is in use - // \rmfamily --> create a new typestyle - // ... --> the new typestyle is in use - // } --> restore the last typestyle - case 'font-dclr': - case 'sizing-dclr': - var cmdName = node.value; - this._textStyle.updateByCommand(cmdName); - this._html.beginSpan(null, this._textStyle.toCSS()); - var textEnv = new TextEnvironment(this._nodes, this._textStyle); - this._html.putSpan(textEnv.renderToHTML()); - this._html.endSpan(); - break; - case 'font-cmd': - var textNode = this._nodes[0]; - if (textNode.type !== 'close-text') continue; - - var cmdName = node.value; - var innerTextStyle = new TextStyle(this._textStyle.fontSize()); - innerTextStyle.updateByCommand(cmdName); - this._html.beginSpan(null, innerTextStyle.toCSS()); - var textEnv = new TextEnvironment(textNode.children, innerTextStyle); - this._html.putSpan(textEnv.renderToHTML()); - this._html.endSpan(); - break; - default: - throw new ParseError('Unexpected ParseNode of type ' + node.type); - } - } - - return this._html.toMarkup(); -}; - -/* HTMLBuilder - A helper class for constructing HTML */ -function HTMLBuilder() { - this._body = []; - this._textBuf = []; -} - -HTMLBuilder.prototype.beginDiv = function(className, style, extraStyle) { - this._beginTag('div', className, style, extraStyle); - this._body.push('\n'); // make the generated HTML more human friendly - return this; -}; - -HTMLBuilder.prototype.endDiv = function() { - this._endTag('div'); - this._body.push('\n'); // make the generated HTML more human friendly - return this; -}; - -HTMLBuilder.prototype.beginP = function(className, style, extraStyle) { - this._beginTag('p', className, style, extraStyle); - this._body.push('\n'); // make the generated HTML more human friendly - return this; -}; - -HTMLBuilder.prototype.endP = function() { - this._flushText(); - this._endTag('p'); - this._body.push('\n'); // make the generated HTML more human friendly - return this; -}; - -HTMLBuilder.prototype.beginSpan = function(className, style, extraStyle) { - this._flushText(); - return this._beginTag('span', className, style, extraStyle); -}; - -HTMLBuilder.prototype.endSpan = function() { - this._flushText(); - return this._endTag('span'); -} - -HTMLBuilder.prototype.putHTML = -HTMLBuilder.prototype.putSpan = function(html) { - this._flushText(); - this._body.push(html); - return this; -} - -HTMLBuilder.prototype.putText = function(text) { - this._textBuf.push(text); - return this; -} - -HTMLBuilder.prototype.write = function(html) { - this._body.push(html); -} - -HTMLBuilder.prototype.toMarkup = function() { - this._flushText(); - var html = this._body.join(''); - return html; -} - -HTMLBuilder.prototype.toDOM = function() { - var html = this.toMarkup(); - var div = document.createElement('div'); - div.innerHTML = html; - return div.firstChild; -} - -HTMLBuilder.prototype._flushText = function(text) { - if (this._textBuf.length == 0) return; - - var text = this._textBuf.join(''); - this._body.push(escapeHtml(text)); - // this._body.push(text); - this._textBuf = []; -} - -/* Write the beginning of a DOM element - tag - the tag of the element - className - the className for the tag - style - CSS style that applies directly on the tag. This parameter can be - either a string, e.g., 'color:red', or an object, e.g. - { color: 'red', margin-left: '1em'} -*/ -HTMLBuilder.prototype._beginTag = function(tag, className, style, extraStyle) { - var spanHTML = '<' + tag; - if (className) spanHTML += ' class="' + className + '"'; - if (style) { - var styleCode; - if (isString(style)) styleCode = style; - else { // style - styleCode = ''; - for (var attrName in style) { - attrVal = style[attrName]; - styleCode += attrName + ':' + attrVal + ';'; - } - } - if (extraStyle) styleCode += extraStyle; - spanHTML += ' style="' + styleCode + '"'; - } - spanHTML += '>'; - this._body.push(spanHTML); - return this; -} - -HTMLBuilder.prototype._endTag = function(tag) { - this._body.push('</' + tag + '>'); - return this; -} - -/* - The renderer converts a parse tree to HTML. - - There are three levels in renderer: - Group (Block), Line and Segment, - which are rendered to HTML tag, <div>, <p>, and <span>, respectively. - -*/ -function Renderer(parser, options) { - this._root = parser.parse(); - // debug - console.log(this._root.toString()); - this._options = new RendererOptions(options); - this._openLine = false; - this._blockLevel = 0; - this._textLevel = -1; - this._globalTextStyle = new TextStyle(); -} - -/* The global counter for the numbering of the algorithm environment */ -Renderer._captionCount = 0; - -Renderer.prototype.toMarkup = function() { - var html = this._html = new HTMLBuilder(); - this._buildTree(this._root); - delete this._html; - return html.toMarkup(); -} - -Renderer.prototype.toDOM = function() { - var html = this.toMarkup(); - var div = document.createElement('div'); - div.innerHTML = html; - return div.firstChild; -} - -Renderer.prototype._beginGroup = function(name, extraClass, style) { - this._closeLineIfAny(); - this._html.beginDiv('ps-' + name + (extraClass ? ' ' + extraClass : ''), - style); -} - -Renderer.prototype._endGroup = function(name) { - this._closeLineIfAny(); - this._html.endDiv(); -} - -Renderer.prototype._beginBlock = function() { - // The first block have to extra left margin when line number are displayed - var extraIndentForFirstBlock = - this._options.lineNumber && this._blockLevel == 0 ? 0.6 : 0; - var blockIndent = this._options.indentSize + extraIndentForFirstBlock; - - this._beginGroup('block', null, { - 'margin-left': blockIndent + 'em' - }); - this._blockLevel++; -} - -Renderer.prototype._endBlock = function() { - this._closeLineIfAny(); - this._endGroup(); - this._blockLevel--; -} - -Renderer.prototype._newLine = function() { - this._closeLineIfAny(); - - this._openLine = true; - - // For every new line, reset the relative sizing of text style - this._globalTextStyle.outerFontSize(1.0); - - var indentSize = this._options.indentSize; - // if this line is for code (e.g. \STATE) - if (this._blockLevel > 0) { - this._numLOC++; - - this._html.beginP('ps-line ps-code', this._globalTextStyle.toCSS()); - if (this._options.lineNumber) { - this._html.beginSpan('ps-linenum', { - 'left': - ((this._blockLevel - 1)*(indentSize - 0.2)) + 'em' - }) - .putText(this._numLOC + this._options.lineNumberPunc) - .endSpan(); - } - } - // if this line is for pre-conditions (e.g. \REQUIRE) - else { - this._html.beginP('ps-line', { - 'text-indent': (-indentSize) + 'em', - 'padding-left': indentSize + 'em' - }, this._globalTextStyle.toCSS()); - } -} - -Renderer.prototype._closeLineIfAny = function() { - if (!this._openLine) return; - - this._html.endP(); - - this._openLine = false; -} - -Renderer.prototype._typeKeyword = function(keyword) { - this._html.beginSpan('ps-keyword').putText(keyword).endSpan(); -} - -Renderer.prototype._typeFuncName = function(funcName) { - this._html.beginSpan('ps-funcname').putText(funcName).endSpan(); -} - -Renderer.prototype._typeText = function(text) { - this._html.write(text); -} - -Renderer.prototype._buildTreeForAllChildren = function(node) { - var children = node.children; - for (var ci = 0; ci < children.length; ci++) - this._buildTree(children[ci]); -} - -Renderer.prototype._buildTree = function(node) { - switch(node.type) { - // The hierarchicy of build tree: Group (Block) > Line > Text - // ----------------- Groups ------------------------------------- - case 'root': - this._beginGroup('root'); - this._buildTreeForAllChildren(node); - this._endGroup(); - break; - case 'algorithm': - // First, decide the caption if any - var lastCaptionNode; - for (var ci = 0; ci < node.children.length; ci++) { - var child = node.children[ci]; - if (child.type !== 'caption') continue; - lastCaptionNode = child; - Renderer._captionCount++; - } - // Then, build the header for algorithm - if (lastCaptionNode) { - this._beginGroup('algorithm', 'with-caption'); - this._buildTree(lastCaptionNode); - } - else { - this._beginGroup('algorithm'); - } - // Then, build other nodes - for (var ci = 0; ci < node.children.length; ci++) { - var child = node.children[ci]; - if (child.type === 'caption') continue; - this._buildTree(child); - } - this._endGroup(); - break; - case 'algorithmic': - if (this._options.lineNumber) { - this._beginGroup('algorithmic', 'with-linenum'); - this._numLOC = 0; - } - else { - this._beginGroup('algorithmic'); - } - this._buildTreeForAllChildren(node); - this._endGroup(); - break; - case 'block': - // node: <block> - // ==> - // HTML: <div class="ps-block"> ... </div> - this._beginBlock(); - this._buildTreeForAllChildren(node); - this._endBlock(); - break; - // ----------------- Mixture (Groups + Lines) ------------------- - case 'function': - // \FUNCTION{<ordinary>}{<text>} <block> \ENDFUNCTION - // ==> - // function <ordinary>(<text>) - // ... - // end function - var funcType = node.value.type.toLowerCase(); - var funcName = node.value.name; - var textNode = node.children[0]; - var blockNode = node.children[1]; - this._newLine(); - this._typeKeyword(funcType + ' '); - this._typeFuncName(funcName); - this._typeText('('); - this._buildTree(textNode); - this._typeText(')'); - - this._buildTree(blockNode); - - if (!this._options.noEnd) { - this._newLine(); - this._typeKeyword('end ' + funcType); - } - break; - case 'if': - // \IF { <cond> } - // ==> - // <p class="ps-line"> - // <span class="ps-keyword">if</span> - // ... - // <span class="ps-keyword">then</span> - // </p> - this._newLine(); - this._typeKeyword('if '); - var cond = node.children[0]; - this._buildTree(cond); - this._typeKeyword(' then'); - // <block> - var ifBlock = node.children[1]; - this._buildTree(ifBlock); - - // ( \ELIF {<cond>} <block> )[0..n] - var numElif = node.value.numElif; - for (var ei = 0 ; ei < numElif; ei++) { - // \ELIF {<cond>} - // ==> - // <p class="ps-line"> - // <span class="ps-keyword">elif</span> - // ... - // <span class="ps-keyword">then</span> - // </p> - this._newLine(); - this._typeKeyword('else if '); - var elifCond = node.children[2 + 2 * ei]; - this._buildTree(elifCond); - this._typeKeyword(' then'); - - // <block> - var elifBlock = node.children[2 + 2 * ei + 1]; - this._buildTree(elifBlock); - } - - // ( \ELSE <block> )[0..1] - var hasElse = node.value.hasElse; - if (hasElse) { - // \ELSE - // ==> - // <p class="ps-line"> - // <span class="ps-keyword">else</span> - // </p> - this._newLine(); - this._typeKeyword('else'); - - // <block> - var elseBlock = node.children[node.children.length - 1]; - this._buildTree(elseBlock); - } - - if (!this._options.noEnd) { - // ENDIF - this._newLine(); - this._typeKeyword('end if'); - } - break; - case 'loop': - // \FOR{<cond>} or \WHILE{<cond>} - // ==> - // <p class="ps-line"> - // <span class="ps-keyword">for</span> - // ... - // <span class="ps-keyword">do</span> - // </p> - this._newLine(); - var loopType = node.value; - var displayLoopName = { - 'FOR': 'for', - 'FORALL': 'for all', - 'WHILE': 'while' - }; - this._typeKeyword(displayLoopName[loopType] + ' '); - var cond = node.children[0]; - this._buildTree(cond); - this._typeKeyword(' do'); - - // <block> - var block = node.children[1]; - this._buildTree(block); - - if (!this._options.noEnd) { - // \ENDFOR or \ENDWHILE - // ==> - // <p class="ps-line"> - // <span class="ps-keyword">end for</span> - // </p> - this._newLine(); - var endLoopName = loopType === 'while' ? 'end while' : 'end for'; - this._typeKeyword(endLoopName); - } - break; - // ------------------- Lines ------------------- - case 'command': - // commands: \STATE, \ENSURE, \PRINT, \RETURN, etc. - var cmdName = node.value; - var displayName = { - 'STATE': '', - 'ENSURE': 'Ensure:', - 'REQUIRE': 'Require:', - 'PRINT': 'print', - 'RETURN': 'return' - }[cmdName]; - - this._newLine(); - if (displayName) this._typeKeyword(displayName); - var text = node.children[0]; - this._buildTree(text); - break; - case 'caption': - this._newLine(); - this._typeKeyword('Algorithm ' + Renderer._captionCount + ' '); - var textNode = node.children[0]; - this._buildTree(textNode); - break; - case 'comment': - var textNode = node.children[0]; - this._html.beginSpan('ps-comment'); - this._html.putText(this._options.commentSymbol); - this._buildTree(textNode); - this._html.endSpan(); - break; - case 'call': - // \CALL{funcName}{funcArgs} - // ==> - // funcName(funcArgs) - var funcName = node.value; - var argsNode = node.children[0]; - if (node.whitespace) this._typeText(' '); - this._typeFuncName(funcName); - this._typeText('('); - this._buildTree(argsNode); - this._typeText(')'); - break; - // ------------------- Text ------------------- - case 'open-text': - var textEnv = new TextEnvironment(node.children, this._globalTextStyle); - this._html.putSpan(textEnv.renderToHTML()); - break; - case 'close-text': - var outerFontSize = this._globalTextStyle.fontSize(); - var newTextStyle = new TextStyle(outerFontSize); - var textEnv = new TextEnvironment(node.children, newTextStyle); - this._html.putSpan(textEnv.renderToHTML()); - break; - default: - throw new ParseError('Unexpected ParseNode of type ' + node.type); - } -} - -// =========================================================================== -// Entry points -// =========================================================================== -parentModule.PseudoCode = { +module.exports = { + ParseError: ParseError, renderToString: function(input, options) { - if (input == null) throw 'input cannot be empty'; + if (input === null || input === undefined) + throw 'input cannot be empty'; var lexer = new Lexer(input); var parser = new Parser(lexer); @@ -1358,15 +15,14 @@ parentModule.PseudoCode = { return renderer.toMarkup(); }, render: function(input, baseDomEle, options) { - if (input == null || baseDomEle == null) throw 'argument cannot be null'; + if (input === null || input === undefined) + throw 'input cannot be empty'; var lexer = new Lexer(input); var parser = new Parser(lexer); var renderer = new Renderer(parser, options); var ele = renderer.toDOM(); - baseDomEle.appendChild(ele); + if (baseDomEle) baseDomEle.appendChild(ele); return ele; } }; - -})(window, katex); diff --git a/package.json b/package.json new file mode 100644 index 0000000..acad09c --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "pseudocode", + "version": "0.1.0", + "author": { + "name": "Tate Tian", + "email": "tatetian@gmail.com", + "url": "http://www.tatetian.me" + }, + "description": "Beautiful pseudocode for the Web", + "main": "pseudocode.js", + "repository": { + "type": "git", + "url": "git://github.com/tatetian/PseudoCode.js" + }, + "files": [ + "PseudoCode.js", + "src/" + ], + "devDependencies": { + "browserify": "~9.0.3", + "watchify": "~2.4.0", + "uglify-js": "~2.4.15", + "clean-css": "~2.2.15", + "jshint": "^2.5.6", + "katex": "~0.1.1" + }, + "license": "MIT" +} diff --git a/src/Lexer.js b/src/Lexer.js new file mode 100644 index 0000000..31a9d46 --- /dev/null +++ b/src/Lexer.js @@ -0,0 +1,128 @@ +/** + * The Lexer class tokenizes the input sequentially, looking ahead only one + * token. + */ +var utils = require('./utils'); +var ParseError = require('./ParseError'); + +var Lexer = function(input) { + this._input = input; + this._remain = input; + this._pos = 0; + this._nextAtom = this._currentAtom = null; + this._next(); // get the next atom +}; + +Lexer.prototype.accept = function(type, text) { + if (this._nextAtom.type === type && this._matchText(text)) { + this._next(); + return this._currentAtom.text; + } + return null; +}; + +Lexer.prototype.expect = function(type, text) { + var nextAtom = this._nextAtom; + // The next atom is NOT of the right type + if (nextAtom.type !== type) + throw new ParseError('Expect an atom of ' + type + ' but received ' + + nextAtom.type, this._pos, this._input); + // Check whether the text is exactly the same + if (!this._matchText(text)) + throw new ParseError('Expect `' + text + '` but received `' + + nextAtom.text + '`', this._pos, this._input); + + this._next(); + return this._currentAtom.text; +}; + +Lexer.prototype.get = function() { + return this._currentAtom; +}; + +/* Math pattern + Math environtment like $ $ or \( \) cannot be matched using regular + expression. This object simulates a RegEx object +*/ +var mathPattern = { + exec: function(str) { + if (str.indexOf('$') !== 0) return null; + + var pos = 1; + var len = str.length; + while (pos < len && ( str[pos] != '$' || str[pos - 1] == '\\' ) ) pos++; + + if (pos === len) return null; + return [str.substring(0, pos + 1), str.substring(1, pos)]; + } +}; +var atomRegex = { + // TODO: which is correct? func: /^\\(?:[a-zA-Z]+|.)/, + special: /^(\\\\|\\{|\\}|\\\$|\\&|\\#|\\%|\\_)/, + func: /^\\([a-zA-Z]+)/, + open: /^\{/, + close: /^\}/, + ordinary: /^[^\\{}$&#%_\s]+/, + math: mathPattern ///^\$.*\$/ +}; +var whitespaceRegex = /^\s*/; + +/* Get the next atom */ +Lexer.prototype._next = function() { + // Skip whitespace (zero or more) + var whitespaceLen = whitespaceRegex.exec(this._remain)[0].length; + this._pos += whitespaceLen; + this._remain = this._remain.slice(whitespaceLen); + + // Remember the current atom + this._currentAtom = this._nextAtom; + + // Reach the end of string + if (this._remain === '') { + this._nextAtom = { + type: 'EOF', + text: null, + whitespace: false + }; + return false; + } + + // Try all kinds of atoms + for (var type in atomRegex) { + var regex = atomRegex[type]; + + var match = regex.exec(this._remain); + if (!match) continue; // not matched + + // match[1] is the useful part, e.g. '123' of '$123$', 'it' of '\\it' + var matchText = match[0]; + var usefulText = match[1] ? match[1] : matchText; + + this._nextAtom = { + type: type, /* special, func, open, close, ordinary, math */ + text: usefulText, /* the text value of the atom */ + whitespace: whitespaceLen > 0 /* any whitespace before the atom */ + }; + console.log('type: ' + type + ', text: ' + usefulText); + + this._pos += matchText.length; + this._remain = this._remain.slice(match[0].length); + + return true; + } + + throw new ParseError('Unrecoganizable atom', this._pos, this._input); +}; + +/* Check whether the text of the next atom matches */ +Lexer.prototype._matchText = function(text) { + // don't need to match + if (text === null || text === undefined) return true; + + if (utils.isString(text)) // is a string, exactly the same? + return text === this._nextAtom.text; + else // is a list, match any of them? + return text.indexOf(this._nextAtom.text) >= 0; +}; + +module.exports = Lexer; diff --git a/src/ParseError.js b/src/ParseError.js new file mode 100644 index 0000000..e3ec0ad --- /dev/null +++ b/src/ParseError.js @@ -0,0 +1,21 @@ +function ParseError(message, pos, input) { + var error = 'Error: ' + message; + // If we have the input and a position, make the error a bit fancier + if (pos !== undefined && input !== undefined) { + error += " at position " + pos + ": `"; + + // Insert a combining underscore at the correct position + input = input.slice(0, pos) + "\u21B1" + input.slice(pos); + + // Extract some context from the input and add it to the error + var begin = Math.max(0, pos - 15); + var end = pos + 15; + error += input.slice(begin, end) + "`"; + } + + this.message = error; +} +ParseError.prototype = Object.create(Error.prototype); +ParseError.prototype.constructor = ParseError; + +module.exports = ParseError; diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 0000000..d52863f --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,458 @@ +/** + * The Parser class parses the token stream from Lexer into an abstract syntax + * tree, represented by ParseNode. + * + * The grammar of pseudocode required by Pseudocode.js mimics that of TeX/Latex + * and its algorithm packages. It is designed intentionally to be less powerful + * than Tex/LaTeX for the convinience of implementation. As a consequence, the + * grammar is context-free, which can be expressed in production rules: + * + * <pseudo> :== ( <algorithm> | <algorithmic> )[0..n] + * + * <algorithm> :== \begin{algorithm} + * + ( <caption> | <algorithmic> )[0..n] + * \end{algorithm} + * <caption> :== \caption{ <close-text> } + * + * <algorithmic> :== \begin{algorithmic} + * + ( <ensure> | <require> | <block> )[0..n] + * + \end{algorithmic} + * <require> :== \REQUIRE + <open-text> + * <ensure> :== \ENSURE + <open-text> + * + * <block> :== ( <control> | <function> + * | <statement> | <comment> | <call> )[0..n] + * + * <control> :== <if> | <for> | <while> + * <if> :== \IF{<cond>} + <block> + * + ( \ELIF{<cond>} <block> )[0..n] + * + ( \ELSE <block> )[0..1] + * + \ENDIF + * <for> :== \FOR{<cond>} + <block> + \ENDFOR + * <while> :== \WHILE{<cond>} + <block> + \ENDWHILE + * + * <function> :== \FUNCTION{<name>}{<params>} <block> \ENDFUNCTION + * (same for <procedure>) + * + * <statement> :== <state> | <return> | <print> + * <state> :== \STATE + <open-text> + * <return> :== \RETURN + <open-text> + * <print> :== \PRINT + <open-text> + * + * <comment> :== \COMMENT{<close-text>} + * + * <call> :== \CALL{<name>}({<close-text>})[0..1] + * + * <cond> :== <close-text> + * <open-text> :== <atom> + <open-text> | { <close-text> } | <empty> + * <close-text> :== <atom> + <close-text> | { <close-text> } | <empty> + * + * <atom> :== <ordinary>[1..n] | <special> | <symbol> + * | <size> | <font> | <bool> | <math> + * <name> :== <ordinary> + * + * <special> :== \\ | \{ | \} | \$ | \& | \# | \% | \_ + * <cond-symbol> :== \AND | \OR | \NOT | \TRUE | \FALSE | \TO + * <text-symbol> :== \textbackslash + * (More LaTeX symbols can be added if necessary. See + * http://get-software.net/info/symbols/comprehensive/symbols-a4.pdf.) + * <math> :== \( + ... + \) | $ ... $ + * (Math are handled by KaTeX) + * <size> :== \tiny | \scriptsize | \footnotesize | \small + * | \normalsize | \large | \Large | \LARGE | \huge + * | \HUGE + * <font> :== \rmfamily | \sffamily | \ttfamily + * | \upshape | \itshape | \slshape | \scshape + * <ordinary> :== not any of \ { } $ & # % _ + * <empty> :== + * + * There are many well-known ways to parse a context-free grammar, like the + * top-down approach LL(k) or the bottom-up approach like LR(k). Both methods + * are usually implemented in a table-driven fashion, which is not suitable to + * write by hand. As our grammar is simple enough and its input is not expected + * to be large, the performance wouldn't be a problem. Thus, I choose to write + * the parser in the most natural form--- a (predictive) recursive descent + * parser. The major benefit of a recursive descent parser is **simplity** for + * the structure of resulting program closely mirrors that of the grammar. * + * + */ +var utils = require('./utils'); + +var ParseNode = function(type, val) { + this.type = type; + this.value = val; + this.children = []; +}; + +ParseNode.prototype.toString = function(level) { + if (!level) level = 0; + + var indent = ''; + for (var i = 0; i < level; i++) indent += ' '; + + var res = indent + '<' + this.type + '>'; + if (this.value) res += ' (' + utils.toString(this.value) + ')'; + res += '\n'; + + if (this.children) { + for (var ci = 0; ci < this.children.length; ci++) { + var child = this.children[ci]; + res += child.toString(level + 1); + } + } + + return res; +}; + +ParseNode.prototype.addChild = function(childNode) { + if (!childNode) throw 'argument cannot be null'; + this.children.push(childNode); +}; + +/* AtomNode is the leaf node of parse tree */ +var AtomNode = function(type, value, whitespace) { + // ParseNode.call(this, type, val); + this.type = type; + this.value = value; + this.children = null; // leaf node, thus no children + this.whitespace = !!whitespace; // is there any whitespace before the atom +}; +AtomNode.prototype = ParseNode.prototype; + +var Parser = function(lexer) { + this._lexer = lexer; +}; + +Parser.prototype.parse = function() { + var root = new ParseNode('root'); + + while (true) { + var envName = this._acceptEnvironment(); + if (envName === null) break; + + var envNode; + if (envName === 'algorithm') + envNode = this._parseAlgorithmInner(); + else if (envName === 'algorithmic') + envNode = this._parseAlgorithmicInner(); + else + throw new ParseError('Unexpected environment ' + envName); + + this._closeEnvironment(envName); + root.addChild(envNode); + } + this._lexer.expect('EOF'); + return root; +}; + +Parser.prototype._acceptEnvironment = function() { + var lexer = this._lexer; + // \begin{XXXXX} + if (!lexer.accept('func', 'begin')) return null; + + lexer.expect('open'); + var envName = lexer.expect('ordinary'); + lexer.expect('close'); + return envName; +}; + +Parser.prototype._closeEnvironment = function(envName) { + // \close{XXXXX} + var lexer = this._lexer; + lexer.expect('func', 'end'); + lexer.expect('open'); + lexer.expect('ordinary', envName); + lexer.expect('close'); +}; + +Parser.prototype._parseAlgorithmInner = function() { + var algNode = new ParseNode('algorithm'); + while (true) { + var envName = this._acceptEnvironment(); + if (envName !== null) { + if (envName !== 'algorithmic') + throw new ParseError('Unexpected environment ' + envName); + var algmicNode = this._parseAlgorithmicInner(); + this._closeEnvironment(); + algNode.addChild(algmicNode); + continue; + } + + var captionNode = this._parseCaption(); + if (captionNode) { + algNode.addChild(captionNode); + continue; + } + + break; + } + return algNode; +}; + +Parser.prototype._parseAlgorithmicInner = function() { + var algmicNode = new ParseNode('algorithmic'); + var node; + while (true) { + node = this._parseCommand(INPUTS_OUTPUTS_COMMANDS); + if (node) { + algmicNode.addChild(node); + continue; + } + + node = this._parseBlock(); + if (node.children.length > 0) { + algmicNode.addChild(node); + continue; + } + + break; + } + return algmicNode; +}; + +Parser.prototype._parseCaption = function() { + var lexer = this._lexer; + if (!lexer.accept('func', 'caption')) return null; + + var captionNode = new ParseNode('caption'); + lexer.expect('open'); + captionNode.addChild(this._parseCloseText()); + lexer.expect('close'); + + return captionNode; +}; + +Parser.prototype._parseBlock = function() { + var blockNode = new ParseNode('block'); + + while (true) { + var controlNode = this._parseControl(); + if (controlNode) { blockNode.addChild(controlNode); continue; } + + var functionNode = this._parseFunction(); + if (functionNode) { blockNode.addChild(functionNode); continue; } + + var commandNode = this._parseCommand(STATEMENT_COMMANDS); + if (commandNode) { blockNode.addChild(commandNode); continue; } + + var commentNode = this._parseComment(); + if (commentNode) { blockNode.addChild(commentNode); continue; } + + var callNode = this._parseCall(); + if (callNode) { blockNode.addChild(callNode); continue; } + + break; + } + + return blockNode; +}; + +Parser.prototype._parseControl = function() { + var controlNode; + if ((controlNode = this._parseIf())) return controlNode; + if ((controlNode = this._parseLoop())) return controlNode; +}; + +Parser.prototype._parseFunction = function() { + var lexer = this._lexer; + if (!lexer.accept('func', ['FUNCTION', 'PROCEDURE'])) return null; + + // \FUNCTION{funcName}{funcArgs} + var funcType = this._lexer.get().text; // FUNCTION or PROCEDURE + lexer.expect('open'); + var funcName = lexer.expect('ordinary'); + lexer.expect('close'); + lexer.expect('open'); + var argsNode = this._parseCloseText(); + lexer.expect('close'); + // <block> + var blockNode = this._parseBlock(); + // \ENDFUNCTION + lexer.expect('func', 'END' + funcType); + + var functionNode = new ParseNode('function', + {type: funcType, name: funcName}); + functionNode.addChild(argsNode); + functionNode.addChild(blockNode); + return functionNode; +}; + +Parser.prototype._parseIf = function() { + if (!this._lexer.accept('func', 'IF')) return null; + + var ifNode = new ParseNode('if'); + + // { <cond> } <block> + this._lexer.expect('open'); + ifNode.addChild(this._parseCond()); + this._lexer.expect('close'); + ifNode.addChild(this._parseBlock()); + + // ( \ELIF { <cond> } <block> )[0...n] + var numElif = 0; + while (this._lexer.accept('func', 'ELIF')) { + this._lexer.expect('open'); + ifNode.addChild(this._parseCond()); + this._lexer.expect('close'); + ifNode.addChild(this._parseBlock()); + numElif++; + } + + // ( \ELSE <block> )[0..1] + var hasElse = false; + if (this._lexer.accept('func', 'ELSE')) { + hasElse = true; + ifNode.addChild(this._parseBlock()); + } + + // \ENDIF + this._lexer.expect('func', 'ENDIF'); + + ifNode.value = {numElif: numElif, hasElse: hasElse}; + return ifNode; +}; + +Parser.prototype._parseLoop = function() { + if (!this._lexer.accept('func', ['FOR', 'FORALL', 'WHILE'])) return null; + + var loopName = this._lexer.get().text; + var loopNode = new ParseNode('loop', loopName); + + // { <cond> } <block> + this._lexer.expect('open'); + loopNode.addChild(this._parseCond()); + this._lexer.expect('close'); + loopNode.addChild(this._parseBlock()); + + // \ENDFOR + var endLoop = loopName !== 'FORALL' ? 'END' + loopName : 'ENDFOR'; + this._lexer.expect('func', endLoop); + + return loopNode; +}; + +var INPUTS_OUTPUTS_COMMANDS = ['ENSURE', 'REQUIRE']; +var STATEMENT_COMMANDS = ['STATE', 'PRINT', 'RETURN']; +Parser.prototype._parseCommand = function(acceptCommands) { + if (!this._lexer.accept('func', acceptCommands)) return null; + + var cmdName = this._lexer.get().text; + var cmdNode = new ParseNode('command', cmdName); + cmdNode.addChild(this._parseOpenText()); + return cmdNode; +}; + +Parser.prototype._parseComment = function() { + if (!this._lexer.accept('func', 'COMMENT')) return null; + + var commentNode = new ParseNode('comment'); + + // { \text } + this._lexer.expect('open'); + commentNode.addChild(this._parseCloseText()); + this._lexer.expect('close'); + + return commentNode; +}; + +Parser.prototype._parseCall = function() { + var lexer = this._lexer; + if (!lexer.accept('func', 'CALL')) return null; + + var anyWhitespace = lexer.get().whitespace; + + // \CALL { <ordinary> } ({ <text> })[0..1] + lexer.expect('open'); + var funcName = lexer.expect('ordinary'); + lexer.expect('close'); + + var callNode = new ParseNode('call'); + callNode.whitespace = anyWhitespace; + callNode.value = funcName; + + lexer.expect('open'); + var argsNode = this._parseCloseText(); + callNode.addChild(argsNode); + lexer.expect('close'); + return callNode; +}; + +Parser.prototype._parseCond = +Parser.prototype._parseCloseText = function() { + return this._parseText('close'); +}; +Parser.prototype._parseOpenText = function() { + return this._parseText('open'); +}; + +Parser.prototype._parseText = function(openOrClose) { + var textNode = new ParseNode(openOrClose + '-text'); + + var atomNode; + while (true) { + atomNode = this._parseAtom(); + if (atomNode) { + textNode.addChild(atomNode); + continue; + } + + if (this._lexer.accept('open')) { + var subTextNode = this._parseCloseText(); + textNode.addChild(subTextNode); + this._lexer.expect('close'); + continue; + } + + break; + } + + return textNode; +}; + +/* The token accepted by atom of specific type */ +var ACCEPTED_TOKEN_BY_ATOM = { + 'ordinary': { tokenType: 'ordinary' }, + 'math': { tokenType: 'math' }, + 'special': { tokenType: 'special' }, + 'cond-symbol': { + tokenType: 'func', + tokenValues: ['AND', 'OR', 'NOT', 'TRUE', 'FALSE', 'TO'] + }, + 'sizing-dclr': { + tokenType: 'func', + tokenValues: ['tiny', 'scriptsize', 'footnotesize', 'small', 'normalsize', + 'large', 'Large', 'LARGE', 'huge', 'Huge'] + }, + 'font-dclr': { + tokenType: 'func', + tokenValues: ['normalfont', 'rmfamily', 'sffamily', 'ttfamily', + 'upshape', 'itshape', 'slshape', 'scshape', + 'bfseries', 'mdseries', 'lfseries'] + }, + 'font-cmd': { + tokenType: 'func', + tokenValues: ['textnormal', 'textrm', 'textsf', 'texttt', 'textup', 'textit', + 'textsl', 'textsc', 'uppercase', 'lowercase', 'textbf', 'textmd', + 'textlf'] + }, + 'text-symbol': { + tokenType: 'func', + tokenValues: ['textbackslash'] + } +}; + +Parser.prototype._parseAtom = function() { + for (var atomType in ACCEPTED_TOKEN_BY_ATOM) { + var acceptToken = ACCEPTED_TOKEN_BY_ATOM[atomType]; + var tokenText = this._lexer.accept( + acceptToken.tokenType, + acceptToken.tokenValues); + if (tokenText === null) continue; + + var anyWhitespace = this._lexer.get().whitespace; + return new AtomNode(atomType, tokenText, anyWhitespace); + } + return null; +}; + +module.exports = Parser; diff --git a/src/Renderer.js b/src/Renderer.js new file mode 100644 index 0000000..a718476 --- /dev/null +++ b/src/Renderer.js @@ -0,0 +1,752 @@ +/* + * TODO: rename commentSymbol to commentDelimiters +* */ +var katex = require('katex'); +var utils = require('./utils'); + +/* + * TextStyle - used by TextEnvironment class to handles LaTeX text-style + * commands or declarations. + * + * The font declarations are: + * \normalfont, \rmfamily, \sffamily, \ttfamily, + * \bfseries, \mdseries, \lfseries, + * \upshape, \itshape, \scshape, \slshape. + * + * The font commands are: + * \textnormal{}, \textrm{}, \textsf{}, \texttt{}, + * \textbf{}, \textmd{}, \textlf{}, + * \textup{}, \textit{}, \textsc{}, \textsl{}, + * \uppercase{}, \lowercase{}. + * + * The sizing commands are: + * \tiny, \scriptsize, \footnotesize, \small, \normalsize, + * \large, \Large, \LARGE, \huge, \Huge. + **/ +function TextStyle(outerFontSize) { + this._css = {}; + + this._fontSize = this._outerFontSize = + outerFontSize !== undefined ? outerFontSize : 1.0; +} + +/* + * Remember the font size of outer TextStyle object. + * + * As we use relative font size 'em', the outer span (has its own TextStyle + * object) affects the size of the span to which this TextStyle object attached. + * */ +TextStyle.prototype.outerFontSize = function(size) { + if (size !== undefined) this._outerFontSize = size; + return this._outerFontSize; +}; + +TextStyle.prototype.fontSize = function() { + return this._fontSize; +}; + +/* Update the font state by TeX command + cmd - the name of TeX command that alters current font +*/ +TextStyle.prototype._fontCommandTable = { + // -------------- declaration -------------- + // font-family + normalfont: { 'font-family': 'KaTeX_Main'}, + rmfamily: { 'font-family': 'KaTeX_Main'}, + sffamily: { 'font-family': 'KaTeX_SansSerif'}, + ttfamily: { 'font-family': 'KaTeX_Typewriter'}, + // weight + bfseries: { 'font-weight': 'bold'}, + mdseries: { 'font-weight': 'medium'}, + lfseries: { 'font-weight': 'lighter'}, + // shape + upshape: { 'font-style': 'normal', 'font-variant': 'normal'}, + itshape: { 'font-style': 'italic', 'font-variant': 'normal'}, + scshape: { 'font-style': 'normal', 'font-variant': 'small-caps'}, + slshape: { 'font-style': 'oblique', 'font-variant': 'normal'}, + // -------------- command -------------- + // font-family + textnormal: { 'font-family': 'KaTeX_Main'}, + textrm: { 'font-family': 'KaTeX_Main'}, + textsf: { 'font-family': 'KaTeX_SansSerif'}, + texttt: { 'font-family': 'KaTeX_Typewriter'}, + // weight + textbf: { 'font-weight': 'bold'}, + textmd: { 'font-weight': 'medium'}, + textlf: { 'font-weight': 'lighter'}, + // shape + textup: { 'font-style': 'normal', 'font-variant': 'normal'}, + textit: { 'font-style': 'italic', 'font-variant': 'normal'}, + textsc: { 'font-style': 'normal', 'font-variant': 'small-caps'}, + textsl: { 'font-style': 'oblique', 'font-variant': 'normal'}, + // case + uppercase: { 'text-transform': 'uppercase'}, + lowercase: { 'text-transform': 'lowercase'} +}; + +TextStyle.prototype._sizingScalesTable = { + tiny: 0.68, + scriptsize: 0.80, + footnotesize: 0.85, + small: 0.92, + normalsize: 1.00, + large: 1.17, + Large: 1.41, + LARGE: 1.58, + huge: 1.90, + Huge: 2.28 +}; + +TextStyle.prototype.updateByCommand = function(cmd) { + // Font command + var cmdStyles = this._fontCommandTable[cmd]; + if (cmdStyles !== undefined) { + for (var attr in cmdStyles) + this._css[attr] = cmdStyles[attr]; + return; + } + + // Sizing command + var fontSize = this._sizingScalesTable[cmd]; + if (fontSize !== undefined) { + this._outerFontSize = this._fontSize; + this._fontSize = fontSize; + return; + } + + throw new ParserError('unrecogniazed text-style command'); +}; + +TextStyle.prototype.toCSS = function() { + var cssStr = ''; + for (var attr in this._css) { + var val = this._css[attr]; + if (val === undefined) continue; + cssStr += attr + ':' + val + ';'; + } + if (this._fontSize !== this._outerFontSize) { + cssStr += 'font-size:' + (this._fontSize / this._outerFontSize) + 'em;'; + } + return cssStr; +}; + +/* + * TextEnvironment - renders the children nodes in a ParseNode of type + * 'close-text' or 'open-text' to HTML. + **/ +function TextEnvironment(nodes, textStyle) { + this._nodes = nodes; + this._textStyle = textStyle; +} + +TextEnvironment.prototype.renderToHTML = function() { + this._html = new HTMLBuilder(); + + var node; + while ((node = this._nodes.shift()) !== undefined) { + var type = node.type; + var text = node.value; + + // Insert whitespace before the atom if necessary + if (node.whitespace) this._html.putText(' '); + + switch(type) { + case 'ordinary': + this._html.putText(text); + break; + case 'math': + var mathHTML = katex.renderToString(text); + this._html.putSpan(mathHTML); + break; + case 'cond-symbol': + this._html.beginSpan('ps-keyword') + .putText(text.toLowerCase()) + .endSpan(); + break; + case 'special': + if (text === '\\\\') { + this._html.putHTML('<br/>'); + break; + } + var replace = { + '\\{': '{', + '\\}': '}', + '\\$': '$', + '\\&': '&', + '\\#': '#', + '\\%': '%', + '\\_': '_' + }; + var replaceStr = replace[text]; + this._html.putText(replaceStr); + break; + case 'text-symbol': + var name2Values = { + 'textbackslash': '\\' + }; + var symbolValue = name2Values[text]; + this._html.putText(symbolValue); + break; + case 'close-text': + var newTextStyle = new TextStyle(this._textStyle.fontSize()); + var closeTextEnv = new TextEnvironment( + node.children, newTextStyle); + this._html.putSpan(closeTextEnv.renderToHTML()); + break; + // There are two kinds of typestyle commands: + // command (e.g. \textrm{...}). + // and + // declaration (e.g. { ... \rmfamily ... }) + // + // For typestyle commands, it works as following: + // \textsf --> create a new typestyle + // { --> save the current typestyle, and then use the new one + // ... --> the new typestyle is in use + // } --> restore the last typestyle + // + // For typestyle declaration, it works a little bit diferrently: + // { --> save the current typestyle, and then create and use + // an identical one + // ... --> the new typestyle is in use + // \rmfamily --> create a new typestyle + // ... --> the new typestyle is in use + // } --> restore the last typestyle + case 'font-dclr': + case 'sizing-dclr': + this._textStyle.updateByCommand(text); + this._html.beginSpan(null, this._textStyle.toCSS()); + var textEnvForDclr = new TextEnvironment(this._nodes, this._textStyle); + this._html.putSpan(textEnvForDclr.renderToHTML()); + this._html.endSpan(); + break; + case 'font-cmd': + var textNode = this._nodes[0]; + if (textNode.type !== 'close-text') continue; + + var innerTextStyle = new TextStyle(this._textStyle.fontSize()); + innerTextStyle.updateByCommand(text); + this._html.beginSpan(null, innerTextStyle.toCSS()); + var textEnvForCmd = new TextEnvironment(textNode.children, innerTextStyle); + this._html.putSpan(textEnvForCmd.renderToHTML()); + this._html.endSpan(); + break; + default: + throw new ParseError('Unexpected ParseNode of type ' + node.type); + } + } + + return this._html.toMarkup(); +}; + +/* HTMLBuilder - A helper class for constructing HTML */ +function HTMLBuilder() { + this._body = []; + this._textBuf = []; +} + +HTMLBuilder.prototype.beginDiv = function(className, style, extraStyle) { + this._beginTag('div', className, style, extraStyle); + this._body.push('\n'); // make the generated HTML more human friendly + return this; +}; + +HTMLBuilder.prototype.endDiv = function() { + this._endTag('div'); + this._body.push('\n'); // make the generated HTML more human friendly + return this; +}; + +HTMLBuilder.prototype.beginP = function(className, style, extraStyle) { + this._beginTag('p', className, style, extraStyle); + this._body.push('\n'); // make the generated HTML more human friendly + return this; +}; + +HTMLBuilder.prototype.endP = function() { + this._flushText(); + this._endTag('p'); + this._body.push('\n'); // make the generated HTML more human friendly + return this; +}; + +HTMLBuilder.prototype.beginSpan = function(className, style, extraStyle) { + this._flushText(); + return this._beginTag('span', className, style, extraStyle); +}; + +HTMLBuilder.prototype.endSpan = function() { + this._flushText(); + return this._endTag('span'); +}; + +HTMLBuilder.prototype.putHTML = +HTMLBuilder.prototype.putSpan = function(html) { + this._flushText(); + this._body.push(html); + return this; +}; + +HTMLBuilder.prototype.putText = function(text) { + this._textBuf.push(text); + return this; +}; + +HTMLBuilder.prototype.write = function(html) { + this._body.push(html); +}; + +HTMLBuilder.prototype.toMarkup = function() { + this._flushText(); + var html = this._body.join(''); + return html; +}; + +HTMLBuilder.prototype.toDOM = function() { + var html = this.toMarkup(); + var div = document.createElement('div'); + div.innerHTML = html; + return div.firstChild; +}; + +HTMLBuilder.prototype._flushText = function() { + if (this._textBuf.length === 0) return; + + var text = this._textBuf.join(''); + this._body.push(this._escapeHtml(text)); + // this._body.push(text); + this._textBuf = []; +}; + +/* Write the beginning of a DOM element + tag - the tag of the element + className - the className for the tag + style - CSS style that applies directly on the tag. This parameter can be + either a string, e.g., 'color:red', or an object, e.g. + { color: 'red', margin-left: '1em'} +*/ +HTMLBuilder.prototype._beginTag = function(tag, className, style, extraStyle) { + var spanHTML = '<' + tag; + if (className) spanHTML += ' class="' + className + '"'; + if (style) { + var styleCode; + if (utils.isString(style)) styleCode = style; + else { // style + styleCode = ''; + for (var attrName in style) { + attrVal = style[attrName]; + styleCode += attrName + ':' + attrVal + ';'; + } + } + if (extraStyle) styleCode += extraStyle; + spanHTML += ' style="' + styleCode + '"'; + } + spanHTML += '>'; + this._body.push(spanHTML); + return this; +}; + +HTMLBuilder.prototype._endTag = function(tag) { + this._body.push('</' + tag + '>'); + return this; +}; + +var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + +HTMLBuilder.prototype._escapeHtml = function(string) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return entityMap[s]; + }); +}; + +/* + * RendererOptions - represents options that Renderer accepts. + * + * The following are possible options: + * indentSize - The indent size of inside a control block, e.g. if, for, + * etc. The unit must be in 'em'. Default value: '1.2em'. + * commentSymbol - The delimiters used to start and end a comment region. + * Note that only line comments are supported. Default value: '//'. + * lineNumber - Whether line numbering is enabled. Default value: false. + * lineNumberPunc - The punctuation that follows line number. Default + * value: ':'. + * noEnd - Whether block ending, like `end if`, end procedure`, etc., are + * showned. Default value: false. + * + **/ +function RendererOptions(options) { + options = options || {}; + this.indentSize = options.indentSize ? + this._parseEmVal(options.indentSize) : 1.2; + this.commentSymbol = options.commentSymbol || ' // '; + this.lineNumberPunc = options.lineNumberPunc || ':'; + this.lineNumber = options.lineNumber !== undefined ? options.lineNumber : false; + this.noEnd = options.noEnd !== undefined ? options.noEnd : false; +} + +RendererOptions.prototype._parseEmVal = function(emVal) { + emVal = emVal.trim(); + if (emVal.indexOf('em') !== emVal.length - 2) + throw 'option unit error; no `em` found'; + return Number(emVal.substring(0, emVal.length - 2)); +}; + +/* + * Renderer - Converts a parse tree to HTML + * + * There are three levels for renderer: Group (Block), Line and Segment, + * which are rendered to HTML tag, <div>, <p>, and <span>, respectively. + **/ +function Renderer(parser, options) { + this._root = parser.parse(); + // debug + console.log(this._root.toString()); + this._options = new RendererOptions(options); + this._openLine = false; + this._blockLevel = 0; + this._textLevel = -1; + this._globalTextStyle = new TextStyle(); +} + +/* The global counter for the numbering of the algorithm environment */ +Renderer._captionCount = 0; + +Renderer.prototype.toMarkup = function() { + var html = this._html = new HTMLBuilder(); + this._buildTree(this._root); + delete this._html; + return html.toMarkup(); +}; + +Renderer.prototype.toDOM = function() { + var html = this.toMarkup(); + var div = document.createElement('div'); + div.innerHTML = html; + return div.firstChild; +}; + +Renderer.prototype._beginGroup = function(name, extraClass, style) { + this._closeLineIfAny(); + this._html.beginDiv('ps-' + name + (extraClass ? ' ' + extraClass : ''), + style); +}; + +Renderer.prototype._endGroup = function(name) { + this._closeLineIfAny(); + this._html.endDiv(); +}; + +Renderer.prototype._beginBlock = function() { + // The first block have to extra left margin when line number are displayed + var extraIndentForFirstBlock = + this._options.lineNumber && this._blockLevel === 0 ? 0.6 : 0; + var blockIndent = this._options.indentSize + extraIndentForFirstBlock; + + this._beginGroup('block', null, { + 'margin-left': blockIndent + 'em' + }); + this._blockLevel++; +}; + +Renderer.prototype._endBlock = function() { + this._closeLineIfAny(); + this._endGroup(); + this._blockLevel--; +}; + +Renderer.prototype._newLine = function() { + this._closeLineIfAny(); + + this._openLine = true; + + // For every new line, reset the relative sizing of text style + this._globalTextStyle.outerFontSize(1.0); + + var indentSize = this._options.indentSize; + // if this line is for code (e.g. \STATE) + if (this._blockLevel > 0) { + this._numLOC++; + + this._html.beginP('ps-line ps-code', this._globalTextStyle.toCSS()); + if (this._options.lineNumber) { + this._html.beginSpan('ps-linenum', { + 'left': - ((this._blockLevel - 1)*(indentSize - 0.2)) + 'em' + }) + .putText(this._numLOC + this._options.lineNumberPunc) + .endSpan(); + } + } + // if this line is for pre-conditions (e.g. \REQUIRE) + else { + this._html.beginP('ps-line', { + 'text-indent': (-indentSize) + 'em', + 'padding-left': indentSize + 'em' + }, this._globalTextStyle.toCSS()); + } +}; + +Renderer.prototype._closeLineIfAny = function() { + if (!this._openLine) return; + + this._html.endP(); + + this._openLine = false; +}; + +Renderer.prototype._typeKeyword = function(keyword) { + this._html.beginSpan('ps-keyword').putText(keyword).endSpan(); +}; + +Renderer.prototype._typeFuncName = function(funcName) { + this._html.beginSpan('ps-funcname').putText(funcName).endSpan(); +}; + +Renderer.prototype._typeText = function(text) { + this._html.write(text); +}; + +Renderer.prototype._buildTreeForAllChildren = function(node) { + var children = node.children; + for (var ci = 0; ci < children.length; ci++) + this._buildTree(children[ci]); +}; + +Renderer.prototype._buildTree = function(node) { + var ci, child, textNode; + switch(node.type) { + // The hierarchicy of build tree: Group (Block) > Line > Text + // ----------------- Groups ------------------------------------- + case 'root': + this._beginGroup('root'); + this._buildTreeForAllChildren(node); + this._endGroup(); + break; + case 'algorithm': + // First, decide the caption if any + var lastCaptionNode; + for (ci = 0; ci < node.children.length; ci++) { + child = node.children[ci]; + if (child.type !== 'caption') continue; + lastCaptionNode = child; + Renderer._captionCount++; + } + // Then, build the header for algorithm + if (lastCaptionNode) { + this._beginGroup('algorithm', 'with-caption'); + this._buildTree(lastCaptionNode); + } + else { + this._beginGroup('algorithm'); + } + // Then, build other nodes + for (ci = 0; ci < node.children.length; ci++) { + child = node.children[ci]; + if (child.type === 'caption') continue; + this._buildTree(child); + } + this._endGroup(); + break; + case 'algorithmic': + if (this._options.lineNumber) { + this._beginGroup('algorithmic', 'with-linenum'); + this._numLOC = 0; + } + else { + this._beginGroup('algorithmic'); + } + this._buildTreeForAllChildren(node); + this._endGroup(); + break; + case 'block': + // node: <block> + // ==> + // HTML: <div class="ps-block"> ... </div> + this._beginBlock(); + this._buildTreeForAllChildren(node); + this._endBlock(); + break; + // ----------------- Mixture (Groups + Lines) ------------------- + case 'function': + // \FUNCTION{<ordinary>}{<text>} <block> \ENDFUNCTION + // ==> + // function <ordinary>(<text>) + // ... + // end function + var funcType = node.value.type.toLowerCase(); + var defFuncName = node.value.name; + textNode = node.children[0]; + var blockNode = node.children[1]; + this._newLine(); + this._typeKeyword(funcType + ' '); + this._typeFuncName(defFuncName); + this._typeText('('); + this._buildTree(textNode); + this._typeText(')'); + + this._buildTree(blockNode); + + if (!this._options.noEnd) { + this._newLine(); + this._typeKeyword('end ' + funcType); + } + break; + case 'if': + // \IF { <cond> } + // ==> + // <p class="ps-line"> + // <span class="ps-keyword">if</span> + // ... + // <span class="ps-keyword">then</span> + // </p> + this._newLine(); + this._typeKeyword('if '); + ifCond = node.children[0]; + this._buildTree(ifCond); + this._typeKeyword(' then'); + // <block> + var ifBlock = node.children[1]; + this._buildTree(ifBlock); + + // ( \ELIF {<cond>} <block> )[0..n] + var numElif = node.value.numElif; + for (var ei = 0 ; ei < numElif; ei++) { + // \ELIF {<cond>} + // ==> + // <p class="ps-line"> + // <span class="ps-keyword">elif</span> + // ... + // <span class="ps-keyword">then</span> + // </p> + this._newLine(); + this._typeKeyword('else if '); + var elifCond = node.children[2 + 2 * ei]; + this._buildTree(elifCond); + this._typeKeyword(' then'); + + // <block> + var elifBlock = node.children[2 + 2 * ei + 1]; + this._buildTree(elifBlock); + } + + // ( \ELSE <block> )[0..1] + var hasElse = node.value.hasElse; + if (hasElse) { + // \ELSE + // ==> + // <p class="ps-line"> + // <span class="ps-keyword">else</span> + // </p> + this._newLine(); + this._typeKeyword('else'); + + // <block> + var elseBlock = node.children[node.children.length - 1]; + this._buildTree(elseBlock); + } + + if (!this._options.noEnd) { + // ENDIF + this._newLine(); + this._typeKeyword('end if'); + } + break; + case 'loop': + // \FOR{<cond>} or \WHILE{<cond>} + // ==> + // <p class="ps-line"> + // <span class="ps-keyword">for</span> + // ... + // <span class="ps-keyword">do</span> + // </p> + this._newLine(); + var loopType = node.value; + var displayLoopName = { + 'FOR': 'for', + 'FORALL': 'for all', + 'WHILE': 'while' + }; + this._typeKeyword(displayLoopName[loopType] + ' '); + var loopCond = node.children[0]; + this._buildTree(loopCond); + this._typeKeyword(' do'); + + // <block> + var block = node.children[1]; + this._buildTree(block); + + if (!this._options.noEnd) { + // \ENDFOR or \ENDWHILE + // ==> + // <p class="ps-line"> + // <span class="ps-keyword">end for</span> + // </p> + this._newLine(); + var endLoopName = loopType === 'while' ? 'end while' : 'end for'; + this._typeKeyword(endLoopName); + } + break; + // ------------------- Lines ------------------- + case 'command': + // commands: \STATE, \ENSURE, \PRINT, \RETURN, etc. + var cmdName = node.value; + var displayName = { + 'STATE': '', + 'ENSURE': 'Ensure:', + 'REQUIRE': 'Require:', + 'PRINT': 'print', + 'RETURN': 'return' + }[cmdName]; + + this._newLine(); + if (displayName) this._typeKeyword(displayName); + textNode = node.children[0]; + this._buildTree(textNode); + break; + case 'caption': + this._newLine(); + this._typeKeyword('Algorithm ' + Renderer._captionCount + ' '); + textNode = node.children[0]; + this._buildTree(textNode); + break; + case 'comment': + textNode = node.children[0]; + this._html.beginSpan('ps-comment'); + this._html.putText(this._options.commentSymbol); + this._buildTree(textNode); + this._html.endSpan(); + break; + case 'call': + // \CALL{funcName}{funcArgs} + // ==> + // funcName(funcArgs) + var callFuncName = node.value; + var argsNode = node.children[0]; + if (node.whitespace) this._typeText(' '); + this._typeFuncName(callFuncName); + this._typeText('('); + this._buildTree(argsNode); + this._typeText(')'); + break; + // ------------------- Text ------------------- + case 'open-text': + var openTextEnv = new TextEnvironment(node.children, this._globalTextStyle); + this._html.putSpan(openTextEnv.renderToHTML()); + break; + case 'close-text': + var outerFontSize = this._globalTextStyle.fontSize(); + var newTextStyle = new TextStyle(outerFontSize); + var closeTextEnv = new TextEnvironment(node.children, newTextStyle); + this._html.putSpan(closeTextEnv.renderToHTML()); + break; + default: + throw new ParseError('Unexpected ParseNode of type ' + node.type); + } +}; + +module.exports = Renderer; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d374903 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,22 @@ +function isString(str) { + return (typeof str === 'string') || (str instanceof String); +} + +function isObject(obj) { + return (typeof obj === 'object' && (obj instanceof Object)); +} + +function toString(obj) { + if (!isObject(obj)) return obj + ''; + + var parts = []; + for (var member in obj) + parts.push(member + ': ' + toString(obj[member])); + return parts.join(', '); +} + +module.exports = { + isString: isString, + isObject: isObject, + toString: toString +}; diff --git a/css/PseudoCode.css b/static/pseudocode.css similarity index 100% rename from css/PseudoCode.css rename to static/pseudocode.css diff --git a/test-suite.html b/static/test-suite.html similarity index 90% rename from test-suite.html rename to static/test-suite.html index d29f356..0134173 100644 --- a/test-suite.html +++ b/static/test-suite.html @@ -2,10 +2,10 @@ <html> <head> <meta charset="utf-8"> - <link href="lib/katex/katex.min.css" type="text/css" rel="stylesheet"> - <script src="lib/katex/katex.min.js" type="text/javascript"></script> - <link href="css/PseudoCode.css" type="text/css" rel="stylesheet"> - <script src="PseudoCode.js" type="text/javascript"></script> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.1.1/katex.min.css"> + <script src="http://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.1.1/katex.min.js"></script> + <link href="pseudocode.css" type="text/css" rel="stylesheet"> + <script src="../build/pseudocode.js" type="text/javascript"></script> <title>Test suite of PseudoCode.js</title> </head> <body> @@ -124,16 +124,16 @@ --> <script type="text/javascript"> var testBasics = document.getElementById("test-basics").textContent; - PseudoCode.render(testBasics, document.body, { + pseudocode.render(testBasics, document.body, { lineNumber: false, }); var testCodes = document.getElementById("test-codes").textContent; - PseudoCode.render(testCodes, document.body, { + pseudocode.render(testCodes, document.body, { lineNumber: false, noEnd: false }); var testExamples = document.getElementById("test-examples").textContent; - PseudoCode.render(testExamples, document.body, { + pseudocode.render(testExamples, document.body, { lineNumber: false, noEnd: false }); -- GitLab