% Grimoire --- _[Smooth CoffeeScript](http://autotelicum.github.com/Smooth-CoffeeScript/)_ % % ☕ > This literate program is _interactive_ in its HTML~5~ form. Edit a CoffeeScript segment to try it. You can see the generated JavaScript as you modify a CoffeeScript function by typing 'show name' after its definition. ## Grimoire A Grimoire is a book of magical incantations. This program got the name because it is full of magic. Not the superstitious kind --- but magical constants, assumptions and implicit definitions --- follow its style and conventions at your own peril. As it is with magic, this program does not actually work, it is all sleight of hand. There is a little touch of self-referential magic in here: This literate program documents the internals of its own kind of literate program documents including itself. If you have some CoffeeScript that would be more spiffy packaged in an interactive HTML~5~ document, then be warned: As in magical potions, the ingredients are exotic and intoxicating. They include `ssam` (streaming sam rc script) from `plan9port`, `pandoc` (the Haskell universal format converter) and the enchanted `CodeMirror` --- the embedded editor that shows your code as it _really_ is. #### The Crystal Ball of Hindsight One night, under the full moon, I might make this more approachable. There isn't anything going on in the `sam` structural regular expressions or the `ssam` pipeline that could not be done in, say, a CoffeeScript program. It is this way because the `plan9` toolset is eminent for prototyping and experiments and `pandoc` does most of the work. Creating these documents is a little something I am doing for fun. Building a professional quality publishing pipeline --- while interesting --- would be an order of magnitude more work. This is not like any commercial application I have ever seen or written. The program attempts to change as little as possible in the `markdown` format, but meddles --- quite unwisely --- in the internals of the HTML~5~ created by `pandoc`. This is to create one document that is simultaneously a `markdown` document, a literate CoffeeScript program, and an interactive HTML~5~ application. ### Editor Commandline Commands I use `acme` to stir the ingredients, but you can use any editor/command line you like --- don't blame me for the result. Speaking of don't blame me: If this program does not work for you, then it is not a bug, it is because you don't believe in magic --- because _seriously_ you don't, do you? Below are the commands I use to extract CoffeeScript code, execute it and to format these Grimoire documents. To execute the commands; middle-button select them in the `acme` environment (if you are reading the markdown source code). As you can see, the magical constants start right here. To use the commands with another document, snarf them and `s/grimoire/new-document-name/g`. ##### Simple Extraction This command extracts CoffeeScript to an editor buffer --- only bird tracks followed by the capitalization that you see below are extracted. This is used to avoid extracting code blocks that are spelled in all capitals i.e `~~~~ {.COFFEESCRIPT}`. In the online environment those blocks become read-only and are not executed. ~~~~ {.bash} Edit ,x/^~~+[ ]*{\.[cC]offee[sS]cript.*}$/+,/^~~+$/-p ~~~~ ##### Extract and Run Standalone The next command extracts the CoffeeScript and prepend definitions that are provided by the interactive environment but do not exist in the standard standalone environment. Creates a CoffeeScript file. Compiles to a JavaScript file. Runs the CoffeeScript to produce an HTML and an output file. You can edit this bit if the document is not producing HTML. Opens the HTML in your web browser and plumbs it back into `acme`. ~~~~ {.bash} Edit ,>ssam -n 'x/^~~+[ ]*{\.[cC]offee[sS]cript.*}$/+,/^~~+$/-' |cat embed-standalone.coffee - |tee grimoire.coffee | coffee -cs >grimoire.js; coffee grimoire.coffee |tee grimoire-output.html >grimoire.output; open grimoire-output.html; plumb grimoire-output.html ~~~~ ##### Create PDF with TeX The following command produces a PDF version of the document. It uses XeTeX which in this case must be present on your system together with too many TeX packages to list (try a full 1.6Gb install if you have the bandwidth or spend hours as I do: start from a basic TeX and add individual packages one-by-one in the Tex Live Utility). It is not required unless you want a PDF version. By the way XeTeX has some trouble with Unicode characters, sometimes. Anyway it assumes that the command above has been run, the `VerbatimInput` lines near the end of this document (you can only see them in the markdown not in the HTML because they are TeX commands) uses the created files to insert program output and JavaScript translation into the PDF. You can delete those lines if you don't want that. ~~~~ {.bash} Edit ,>markdown2pdf --listings --xetex '--template=pandoc-template.tex' -o grimoire.pdf; open grimoire.pdf ~~~~ ##### Create an HTML~5~ Application The last command produces an interactive HTML~5~ document. It does assume that the 2^nd^ commands above has been run sometime earlier (to bootstrap itself). The command below works as a straight pipeline. First running the markdown through `pandoc` to produce HTML~5~ with `section` and `mathml` elements[^1]. It creates a standalone document based on the template in `pandoc-template.html` and includes different CSS files for styling itself and its embedded editor. It also includes itself with the `-B` (`include-before-body`) option, not the interactive HTML that you might be reading, but the output-HTML that the interactive HTML produces. [^1]: MathML is at this time not rendered in the two consumer browsers, Microsoft Internet Explorer and Google Chrome. I have tried MathJax but that resulted in Opera displaying garbage instead of equations. I found that it was unacceptable that Opera which does show W3C standard MathML get degraded, just because Google and Microsoft have chosen not to implement the standard. Of course in a commercial project the decision would likely be the other way round because of Opera's tiny market share compared with IE and Chrome. MathML renders fine in Firefox, Opera, and Safari on OS X & iOS. The next four stages are all `ssam` substitutions in the HTML that `pandoc` produced. Talk about being dependent on internal implementation details --- the `pandoc` used is version is 1.8.2. (@) The relevant `` elements (based on their class and spelling) are made contenteditable. That is only used to allow them to get focus and could possibly be replaced by pseudo onclick handlers. They are also set to _not_ be `spellcheck`ed, which should be obvious for `` elements. But even with that flag, Firefox still spellchecks the CoffeeScript in the elements displaying a lot of ugly squiggles. (@) Sections with an id that starts with `view-solution` are hidden and given a `reveal` event handler. The hidden part goes on to the next section, which in the markdown can be marked with a header `#####` without a text. That bit is now depending on `pandoc` not optimizing those superfluous headers away. But it makes it possible for solutions to have multiple code blocks and images in them. (@) This part replaces the 1^st^ and only the 1^st^ image reference in a document with a canvas element. This is something that should be changed: a canvas element isn't usable without scripting and this could be done in scripting. (@) The last defaults images to be centered instead of left aligned. This should be done in CSS instead. ~~~~ {.bash} Edit ,>pandoc -f markdown -t html -S -5 --mathml --section-divs --css pandoc-template.css --css codemirror/codemirror.css --css codemirror/pantheme.css --template pandoc-template.html -B grimoire-output.html | ssam 's/()(\n.*\n)(
)/\1 onclick=\"reveal(this)\" \2\3\4 style=\"display:none\" \5/g' | ssam 's/\"[^\"]+\"/<\/canvas>/' | ssam 's/(grimoire.html; open grimoire.html; plumb grimoire.html ~~~~ ### Interactive Environment ~~~~ {.coffeescript} webfragment = -> ~~~~ #### Feature detection warning elements ~~~~ {.coffeescript} div id: "feature-detect", -> div id: "feature-javascript", style: "color:#FF0000; display: block", -> text "No JavaScript → no output and no interactivity." div id: "feature-mathml", style: "color:#FF0000; display: none", -> text "No MathML 1998 → math is not readable." div id: "feature-canvas", style: "color:#FF0000; display: none", -> text "No Canvas → graphical output is not rendered." div id: "feature-contenteditable", style: "color:#FF0000; display: none", -> text "No ContentEditable → CoffeeScript sections can not be changed." ~~~~ #### User settings menu ~~~~ {.coffeescript} input class: 'field', type: 'button', value: 'Adjust layout', onclick: -> @value = if toggleLayout() then 'Layout: fixed' else 'Layout: freeflow' input class: 'field', type: 'button', value: 'Select editor', onclick: -> @value = if switchEditor() then 'Editor: CodeMirror' else 'Editor: plain text' input class: 'field', type: 'button', value: 'Evaluation', onclick: -> @value = if switchEvaluation() then '↑ ↩ Evaluate' else 'Auto Evaluate' ~~~~ #### External scripts These scripts should be the same as those listed in the application manifest, `pandoc-template.appcache` that is mentioned in the `pandoc-template.html` file. ~~~~ {.coffeescript} script src: 'node_modules/coffee-script.js' script src: 'node_modules/coffeekup.js' script src: 'node_modules/underscore.js' script src: 'node_modules/qc.js' script src: 'codemirror/codemirror.js' script src: 'codemirror/coffeescript.js' ~~~~ ### The Interactive Environment ~~~~ {.coffeescript} coffeescript -> ~~~~ #### Hidden sections support ~~~~ {.coffeescript} @reveal = (instance) -> # Very pandoc specific: requires --section-divs instance.getElementsByTagName('section')[0]?.style.display = 'block' instance.getElementsByTagName('h5')[0]?.innerHTML = 'Solution' instance.onclick = undefined ~~~~ #### User settings support ~~~~ {.coffeescript} @switchEditor = -> localStorage?.editor = if @useCodeMirror then 'TextArea' else 'CodeMirror' @useCodeMirror = not @useCodeMirror @switchEvaluation = -> localStorage?.evaluation = if localStorage?.evaluation is 'manual' 'automatic' else 'manual' localStorage?.evaluation is 'manual' @toggleLayout = -> fixedLayout = document.getElementById('page').style.maxWidth is '' localStorage?.fixedLayout = fixedLayout switchLayout fixedLayout switchLayout = (fixedLayout) -> # Should be in CSS document.getElementById('page').style.maxWidth = if fixedLayout then '600px' else '' s = document.getElementById('page').style if fixedLayout s.webkitHyphens = 'auto' s.mozHyphens = 'auto' s.msHyphens = 'auto' s.hyphens = 'auto' s.textAlign = 'justify' else s.webkitHyphens = '' s.mozHyphens = '' s.msHyphens = '' s.hyphens = '' s.textAlign = '' fixedLayout ~~~~ #### Feature Detection ~~~~ {.coffeescript} featureDetect = -> adjustCoverPosition = (idFeature) -> # Should be in CSS canvasCover = document.getElementById('drawCanvas') topPos = parseInt canvasCover?.style.top lineHeight = document.getElementById(idFeature)?.scrollHeight canvasCover?.style.top = (topPos + lineHeight) + 'px' mathmlDetect = -> (e = document.createElement 'div').innerHTML = '' passed = e.firstChild and 'namespaceURI' of e.firstChild and e.firstChild.namespaceURI is 'http://www.w3.org/1998/Math/MathML' # TODO Needs a better test. Testing on Chrome directly because # it reports it supports MathML even though that is not the case. passed and not /Chrome/.test navigator.userAgent document.getElementById('feature-javascript')?.style.display = "none" used = (document.getElementsByTagName('math')?.length > 0) if used and mathmlDetect() is false document.getElementById('feature-mathml')?.style.display = "block" adjustCoverPosition 'feature-mathml' unless document.createElement('canvas').getContext? document.getElementById('feature-canvas')?.style.display = "block" adjustCoverPosition 'feature-canvas' unless 'isContentEditable' of document.documentElement document.getElementById('feature-contenteditable')?.style.display = "block" adjustCoverPosition 'feature-contenteditable' ~~~~ #### Startup ~~~~ {.coffeescript} window.onload = -> featureDetect() switchLayout(on) if localStorage?.fixedLayout isnt 'false' @useCodeMirror = localStorage?.editor is 'CodeMirror' ~~~~ #### Runtime canvas support As mentioned in the command used to create an [HTML~5~ application, item #3](#create-an-html5-application) this should not be done automatically. ~~~~ {.coffeescript} canvas = document.getElementById('drawCanvas') window.ctx = canvas.getContext '2d' if canvas? clearCanvas = -> if window.ctx? window.ctx.clearRect 0, 0, window.ctx.canvas.width, window.ctx.canvas.height drawCanvas = (draw) -> clearCanvas() draw window.ctx ~~~~ #### Runtime output support The `dataurl` in `addFrame` is not displayed in IE9 nor is its `innerHTML`. ~~~~ {.coffeescript} getParent = (child) -> child?.parentElement ? child?.parentNode addElement = (parent, text) -> newelem = document.createElement 'code' newelem.setAttribute 'class', 'sourceCode output' newelem.innerHTML = text parent.appendChild newelem separator = (parent) -> if parent.getElementsByClassName('output').length is 0 addElement parent, '

' addFrame = (parent, width, height, content) -> newelem = document.createElement 'iframe' newelem.setAttribute 'class', 'output' newelem.setAttribute 'width', width newelem.setAttribute 'height', height newelem.setAttribute 'src', "data:text/html;charset=utf-8,#{encodeURIComponent content}" newelem.innerHTML = '''
No or insufficient Data URL support → web page output is not rendered in this browser.
''' parent.appendChild newelem showHere = (atTag, obj, shallow = false, symbol = '→') -> displayShallow = (o) -> """{#{"\n #{k}: #{v}" for own k,v of o}\n}""" msg = switch typeof obj when 'undefined', 'string', 'function' obj when 'object' if shallow displayShallow obj else try JSON.stringify obj catch err displayShallow obj else obj?.toString() parentTag = getParent atTag separator parentTag addElement parentTag, "#{symbol} #{msg}
" obj showDocumentHere = (atTag, content, width = 300, height = 300) -> parentTag = getParent atTag separator parentTag addFrame parentTag, width, height, content return ~~~~ #### CoffeeScript Evaluation This is called during startup for a full evaluation. It is also triggered by changes in one of the editor types, where it can be full evaluation with code stitching for short articles or incremental i.e. one code block for a whole book. ~~~~ {.coffeescript} evaluateSource = (field = null) -> ~~~~ #### Main output functions ~~~~ {.coffeescript} show = (obj, shallow = false, symbol = '→') -> showHere currentTag, obj, shallow, symbol return # Suppress display of the return value view = (obj, shallow = false, symbol = '→') -> showHere currentTag, obj, shallow, symbol showDocument = (content, width = 300, height = 300) -> showDocumentHere currentTag, content, width, height addErrorElement = (text) -> show """#{text}""" ~~~~ #### Callbacks with output related to place of definition ~~~~ {.coffeescript} # Naming convention: 'button-' '-' getCurrentTagId = (id) -> id.match(/button-\w+-(.*)/)[1] runOnDemand = (func) -> show "" document.getElementById("button-run-#{currentTag.id}").onclick = -> currentTag = document.getElementById getCurrentTagId @id view = show = (obj, shallow = false, symbol = '⇒') -> showHere currentTag, obj, shallow, symbol showDocument = (content, width = 300, height = 300) -> showDocumentHere currentTag, content, width, height func() return confirm = (message, func) -> show "#{message}" + " " + " " document.getElementById("button-yes-#{currentTag.id}").onclick = -> currentTag = document.getElementById getCurrentTagId @id view = show = (obj, shallow = false, symbol = '⇒') -> showHere currentTag, obj, shallow, symbol showDocument = (content, width = 300, height = 300) -> showDocumentHere currentTag, content, width, height func true document.getElementById("button-no-#{currentTag.id}").onclick = -> currentTag = document.getElementById getCurrentTagId @id view = show = (obj, shallow = false, symbol = '⇒') -> showHere currentTag, obj, shallow, symbol showDocument = (content, width = 300, height = 300) -> showDocumentHere currentTag, content, width, height func false return prompt = (message, defaultValue, func) -> show "#{message}" + " " + " " document.getElementById("button-prompt-#{currentTag.id}").onclick = -> currentTag = document.getElementById getCurrentTagId @id view = show = (obj, shallow = false, symbol = '⇒') -> showHere currentTag, obj, shallow, symbol showDocument = (content, width = 300, height = 300) -> showDocumentHere currentTag, content, width, height func document.getElementById("input-prompt-#{getCurrentTagId @id}").value return ~~~~ #### QuickCheck You may ask: "What are these completely unrelated QuickCheck helpers doing here?" Well, it could be that I am simply trying to distract you while I pull something out of my sleeve --- or it could be that I didn't get round to moving them into `qc.js`, which as its name implies is written in JavaScript (and it is not a CommonJS module either). Incidentally JavaScript could learn a thing or two from CoffeeScript. For example: it could use a backtick feature for embedding CoffeeScript into it. Until the day when these helpers are moved to a better resting place, at least they are not doing much harm here … unless you summon them. QuickCheck is a minor imp, using randomness to cause havoc in your code. Several examples are shown in 'Smooth CoffeeScript' which is why the helpers are here. Let me know if you should have converted `qc.js` to CoffeeScript or have written something better. ~~~~ {.coffeescript} # HTML colored output for QuickCheck. class HtmlListener extends ConsoleListener constructor: (@maxCollected = 10) -> log: (str) -> show str passed: (str) -> # print message as OK show """#{str}""" invalid: (str) -> # print message as warning show """#{str}""" failure: (str) -> # print message as error show """#{str}""" done: -> show 'Completed test.' resetProps() # Chain here if needed # Enhanced noteArg returning its argument so it can be used inline. Case::note = (a) -> @noteArg a; a # Same as Case::note but also logs the noted args. Case::noteVerbose = (a) -> @noteArg a; show @args; a # Helper to declare a named test property for # a function func taking types as arguments. # Property is passed the testcase, the arguments # and the result of calling func, it must return # a boolean indicating success or failure. testPure = (func, types, name, property) -> declare name, types, (c, a...) -> c.assert property c, a..., c.note func a... return # Default qc configuration with 100 pass and 1000 invalid tests qcConfig = new Config 100, 1000 # Test all known properties test = (msg, func) -> _.each [msg, func, runAllProps qcConfig, new HtmlListener], (o) -> unless _.isUndefined o then show o ~~~~ #### Traversing and unescaping The code for the field or the document is obtained with tags that relate the code back to its HTML element. The tag is needed to direct output to its place of origin. When an editor is instantiated for a `` element it changes to a `