diff --git a/.travis.yml b/.travis.yml index 9bdccd0e3e..baa0031d50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,3 @@ language: node_js node_js: - 0.8 -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" diff --git a/bin/compress b/bin/compress new file mode 100755 index 0000000000..925eee2c37 --- /dev/null +++ b/bin/compress @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +// Compression helper for CodeMirror +// +// Example: +// +// bin/compress codemirror runmode javascript xml +// +// Will take lib/codemirror.js, lib/util/runmode.js, +// mode/javascript/javascript.js, and mode/xml/xml.js, run them though +// the online minifier at http://marijnhaverbeke.nl/uglifyjs, and spit +// out the result. +// +// bin/compress codemirror --local /path/to/bin/UglifyJS +// +// Will use a local minifier instead of the online default one. +// +// Script files are specified without .js ending. Prefixing them with +// their full (local) path is optional. So you may say lib/codemirror +// or mode/xml/xml to be more precise. In fact, even the .js suffix +// may be speficied, if wanted. + +"use strict"; + +var fs = require("fs"); + +function help(ok) { + console.log("usage: " + process.argv[1] + " [--local /path/to/uglifyjs] files..."); + process.exit(ok ? 0 : 1); +} + +var local = null, args = null, files = [], blob = ""; + +for (var i = 2; i < process.argv.length; ++i) { + var arg = process.argv[i]; + if (arg == "--local" && i + 1 < process.argv.length) { + var parts = process.argv[++i].split(/\s+/); + local = parts[0]; + args = parts.slice(1); + } else if (arg == "--help") { + help(true); + } else if (arg[0] != "-") { + files.push({name: arg, re: new RegExp("(?:\\/|^)" + arg + (/\.js$/.test(arg) ? "$" : "\\.js$"))}); + } else help(false); +} + +function walk(dir) { + fs.readdirSync(dir).forEach(function(fname) { + if (/^[_\.]/.test(fname)) return; + var file = dir + fname; + if (fs.statSync(file).isDirectory()) return walk(file + "/"); + if (files.some(function(spec, i) { + var match = spec.re.test(file); + if (match) files.splice(i, 1); + return match; + })) { + if (local) args.push(file); + else blob += fs.readFileSync(file, "utf8"); + } + }); +} + +walk("lib/"); +walk("mode/"); + +if (files.length) { + console.log("Some speficied files were not found: " + + files.map(function(a){return a.name;}).join(", ")); + process.exit(1); +} + +if (local) { + require("child_process").spawn(local, args, {stdio: ["ignore", process.stdout, process.stderr]}); +} else { + var data = new Buffer("js_code=" + require("querystring").escape(blob), "utf8"); + var req = require("http").request({ + host: "marijnhaverbeke.nl", + port: 80, + method: "POST", + path: "/uglifyjs", + headers: {"content-type": "application/x-www-form-urlencoded", + "content-length": data.length} + }); + req.on("response", function(resp) { + resp.on("data", function (chunk) { process.stdout.write(chunk); }); + }); + req.end(data); +} diff --git a/demo/mustache.html b/demo/mustache.html index d9051a23eb..c2ce331077 100644 --- a/demo/mustache.html +++ b/demo/mustache.html @@ -37,6 +37,7 @@

{{title}}

if (stream.match("{{")) { while ((ch = stream.next()) != null) if (ch == "}" && stream.next() == "}") break; + stream.eat("}"); return "mustache"; } while (stream.next() != null && !stream.match("{{", false)) {} diff --git a/doc/compress.html b/doc/compress.html index 3e4abc1479..6c31b77fe8 100644 --- a/doc/compress.html +++ b/doc/compress.html @@ -10,10 +10,13 @@

{ } CodeMirror

-
-/* Script compression
+
+ +
+/* Script compression
    helper */
 
+

To optimize loading CodeMirror, especially when including a bunch of different modes, it is recommended that you combine and @@ -27,28 +30,32 @@

{ } CodeMi

Version:

+ + +

MIME types defined: text/x-common-lisp.

+ + + diff --git a/mode/css/css.js b/mode/css/css.js index 9428c4e32e..87d5d7401e 100644 --- a/mode/css/css.js +++ b/mode/css/css.js @@ -1,60 +1,196 @@ CodeMirror.defineMode("css", function(config) { var indentUnit = config.indentUnit, type; + + var atMediaTypes = keySet([ + "all", "aural", "braille", "handheld", "print", "projection", "screen", + "tty", "tv", "embossed" + ]); + + var atMediaFeatures = keySet([ + "width", "min-width", "max-width", "height", "min-height", "max-height", + "device-width", "min-device-width", "max-device-width", "device-height", + "min-device-height", "max-device-height", "aspect-ratio", + "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", + "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", + "max-color", "color-index", "min-color-index", "max-color-index", + "monochrome", "min-monochrome", "max-monochrome", "resolution", + "min-resolution", "max-resolution", "scan", "grid" + ]); - var keywords = keySet(["above", "absolute", "activeborder", "activecaption", "afar", "after-white-space", "ahead", "alias", "all", "all-scroll", - "alternate", "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", "arabic-indic", "armenian", "asterisks", - "auto", "avoid", "background", "backwards", "baseline", "below", "bidi-override", "binary", "bengali", "blink", - "block", "block-axis", "bold", "bolder", "border", "border-box", "both", "bottom", "break-all", "break-word", "button", - "button-bevel", "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian", "capitalize", "caps-lock-indicator", - "caption", "captiontext", "caret", "cell", "center", "checkbox", "circle", "cjk-earthly-branch", "cjk-heavenly-stem", "cjk-ideographic", - "clear", "clip", "close-quote", "col-resize", "collapse", "compact", "condensed", "contain", "content", "content-box", "context-menu", - "continuous", "copy", "cover", "crop", "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal", "decimal-leading-zero", "default", - "default-button", "destination-atop", "destination-in", "destination-out", "destination-over", "devanagari", "disc", "discard", "document", - "dot-dash", "dot-dot-dash", "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", "element", - "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", "ethiopic-abegede-am-et", "ethiopic-abegede-gez", - "ethiopic-abegede-ti-er", "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", "ethiopic-halehame-aa-et", - "ethiopic-halehame-am-et", "ethiopic-halehame-gez", "ethiopic-halehame-om-et", "ethiopic-halehame-sid-et", - "ethiopic-halehame-so-et", "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", "ew-resize", "expanded", - "extra-condensed", "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes", "forwards", "from", "geometricPrecision", - "georgian", "graytext", "groove", "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", "help", - "hidden", "hide", "higher", "highlight", "highlighttext", "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", - "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", "infobackground", "infotext", "inherit", "initial", "inline", - "inline-axis", "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert", "italic", "justify", "kannada", "katakana", - "katakana-iroha", "khmer", "landscape", "lao", "large", "larger", "left", "level", "lighter", "line-through", "linear", "lines", - "list-item", "listbox", "listitem", "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", "lower-greek", - "lower-hexadecimal", "lower-latin", "lower-norwegian", "lower-roman", "lowercase", "ltr", "malayalam", "match", "media-controls-background", - "media-current-time-display", "media-fullscreen-button", "media-mute-button", "media-play-button", "media-return-to-realtime-button", - "media-rewind-button", "media-seek-back-button", "media-seek-forward-button", "media-slider", "media-sliderthumb", "media-time-remaining-display", - "media-volume-slider", "media-volume-slider-container", "media-volume-sliderthumb", "medium", "menu", "menulist", "menulist-button", - "menulist-text", "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", "mix", "mongolian", "monospace", "move", "multiple", - "myanmar", "n-resize", "narrower", "navy", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", "no-open-quote", "no-repeat", "none", - "normal", "not-allowed", "nowrap", "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", "optimizeLegibility", - "optimizeSpeed", "oriya", "oromo", "outset", "outside", "overlay", "overline", "padding", "padding-box", "painted", "paused", - "persian", "plus-darker", "plus-lighter", "pointer", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", - "push-button", "radio", "read-only", "read-write", "read-write-plaintext-only", "relative", "repeat", "repeat-x", - "repeat-y", "reset", "reverse", "rgb", "rgba", "ridge", "right", "round", "row-resize", "rtl", "run-in", "running", "s-resize", "sans-serif", - "scroll", "scrollbar", "se-resize", "searchfield", "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button", - "searchfield-results-decoration", "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", "single", - "skip-white-space", "slide", "slider-horizontal", "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", - "small", "small-caps", "small-caption", "smaller", "solid", "somali", "source-atop", "source-in", "source-out", "source-over", - "space", "square", "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", "subpixel-antialiased", "super", - "sw-resize", "table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", - "table-row", "table-row-group", "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", "thick", "thin", - "threeddarkshadow", "threedface", "threedhighlight", "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", "tigrinya-er-abegede", - "tigrinya-et", "tigrinya-et-abegede", "to", "top", "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", "upper-alpha", "upper-armenian", - "upper-greek", "upper-hexadecimal", "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", "vertical", "vertical-text", "visible", - "visibleFill", "visiblePainted", "visibleStroke", "visual", "w-resize", "wait", "wave", "white", "wider", "window", "windowframe", "windowtext", - "x-large", "x-small", "xor", "xx-large", "xx-small", "yellow", "-wap-marquee", "-webkit-activelink", "-webkit-auto", "-webkit-baseline-middle", - "-webkit-body", "-webkit-box", "-webkit-center", "-webkit-control", "-webkit-focus-ring-color", "-webkit-grab", "-webkit-grabbing", - "-webkit-gradient", "-webkit-inline-box", "-webkit-left", "-webkit-link", "-webkit-marquee", "-webkit-mini-control", "-webkit-nowrap", "-webkit-pictograph", - "-webkit-right", "-webkit-small-control", "-webkit-text", "-webkit-xxx-large", "-webkit-zoom-in", "-webkit-zoom-out"]); + var propertyKeywords = keySet([ + "align-content", "align-items", "align-self", "alignment-adjust", + "alignment-baseline", "anchor-point", "animation", "animation-delay", + "animation-direction", "animation-duration", "animation-iteration-count", + "animation-name", "animation-play-state", "animation-timing-function", + "appearance", "azimuth", "backface-visibility", "background", + "background-attachment", "background-clip", "background-color", + "background-image", "background-origin", "background-position", + "background-repeat", "background-size", "baseline-shift", "binding", + "bleed", "bookmark-label", "bookmark-level", "bookmark-state", + "bookmark-target", "border", "border-bottom", "border-bottom-color", + "border-bottom-left-radius", "border-bottom-right-radius", + "border-bottom-style", "border-bottom-width", "border-collapse", + "border-color", "border-image", "border-image-outset", + "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-left", "border-left-color", + "border-left-style", "border-left-width", "border-radius", "border-right", + "border-right-color", "border-right-style", "border-right-width", + "border-spacing", "border-style", "border-top", "border-top-color", + "border-top-left-radius", "border-top-right-radius", "border-top-style", + "border-top-width", "border-width", "bottom", "box-decoration-break", + "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", + "caption-side", "clear", "clip", "color", "color-profile", "column-count", + "column-fill", "column-gap", "column-rule", "column-rule-color", + "column-rule-style", "column-rule-width", "column-span", "column-width", + "columns", "content", "counter-increment", "counter-reset", "crop", "cue", + "cue-after", "cue-before", "cursor", "direction", "display", + "dominant-baseline", "drop-initial-after-adjust", + "drop-initial-after-align", "drop-initial-before-adjust", + "drop-initial-before-align", "drop-initial-size", "drop-initial-value", + "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis", + "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", + "float", "float-offset", "font", "font-feature-settings", "font-family", + "font-kerning", "font-language-override", "font-size", "font-size-adjust", + "font-stretch", "font-style", "font-synthesis", "font-variant", + "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", + "font-variant-ligatures", "font-variant-numeric", "font-variant-position", + "font-weight", "grid-cell", "grid-column", "grid-column-align", + "grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow", + "grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span", + "grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens", + "icon", "image-orientation", "image-rendering", "image-resolution", + "inline-box-align", "justify-content", "left", "letter-spacing", + "line-break", "line-height", "line-stacking", "line-stacking-ruby", + "line-stacking-shift", "line-stacking-strategy", "list-style", + "list-style-image", "list-style-position", "list-style-type", "margin", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "marker-offset", "marks", "marquee-direction", "marquee-loop", + "marquee-play-count", "marquee-speed", "marquee-style", "max-height", + "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", + "nav-left", "nav-right", "nav-up", "opacity", "order", "orphans", "outline", + "outline-color", "outline-offset", "outline-style", "outline-width", + "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y", + "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", + "page", "page-break-after", "page-break-before", "page-break-inside", + "page-policy", "pause", "pause-after", "pause-before", "perspective", + "perspective-origin", "pitch", "pitch-range", "play-during", "position", + "presentation-level", "punctuation-trim", "quotes", "rendering-intent", + "resize", "rest", "rest-after", "rest-before", "richness", "right", + "rotation", "rotation-point", "ruby-align", "ruby-overhang", + "ruby-position", "ruby-span", "size", "speak", "speak-as", "speak-header", + "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", + "tab-size", "table-layout", "target", "target-name", "target-new", + "target-position", "text-align", "text-align-last", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-skip", + "text-decoration-style", "text-emphasis", "text-emphasis-color", + "text-emphasis-position", "text-emphasis-style", "text-height", + "text-indent", "text-justify", "text-outline", "text-shadow", + "text-space-collapse", "text-transform", "text-underline-position", + "text-wrap", "top", "transform", "transform-origin", "transform-style", + "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "unicode-bidi", + "vertical-align", "visibility", "voice-balance", "voice-duration", + "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", + "voice-volume", "volume", "white-space", "widows", "width", "word-break", + "word-spacing", "word-wrap", "z-index" + ]); + + var colorKeywords = keySet([ + "black", "silver", "gray", "white", "maroon", "red", "purple", "fuchsia", + "green", "lime", "olive", "yellow", "navy", "blue", "teal", "aqua" + ]); + + var valueKeywords = keySet([ + "above", "absolute", "activeborder", "activecaption", "afar", + "after-white-space", "ahead", "alias", "all", "all-scroll", "alternate", + "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", + "arabic-indic", "armenian", "asterisks", "auto", "avoid", "background", + "backwards", "baseline", "below", "bidi-override", "binary", "bengali", + "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", + "both", "bottom", "break-all", "break-word", "button", "button-bevel", + "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian", + "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", + "cell", "center", "checkbox", "circle", "cjk-earthly-branch", + "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", + "col-resize", "collapse", "compact", "condensed", "contain", "content", + "content-box", "context-menu", "continuous", "copy", "cover", "crop", + "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal", + "decimal-leading-zero", "default", "default-button", "destination-atop", + "destination-in", "destination-out", "destination-over", "devanagari", + "disc", "discard", "document", "dot-dash", "dot-dot-dash", "dotted", + "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", + "element", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", + "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", + "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", + "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", + "ethiopic-halehame-gez", "ethiopic-halehame-om-et", + "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", + "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", + "ethiopic-halehame-tig", "ew-resize", "expanded", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes", + "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", + "help", "hidden", "hide", "higher", "highlight", "highlighttext", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", + "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", + "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", + "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert", + "italic", "justify", "kannada", "katakana", "katakana-iroha", "khmer", + "landscape", "lao", "large", "larger", "left", "level", "lighter", + "line-through", "linear", "lines", "list-item", "listbox", "listitem", + "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", + "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", + "lower-roman", "lowercase", "ltr", "malayalam", "match", + "media-controls-background", "media-current-time-display", + "media-fullscreen-button", "media-mute-button", "media-play-button", + "media-return-to-realtime-button", "media-rewind-button", + "media-seek-back-button", "media-seek-forward-button", "media-slider", + "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", + "media-volume-slider-container", "media-volume-sliderthumb", "medium", + "menu", "menulist", "menulist-button", "menulist-text", + "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", + "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize", + "narrower", "navy", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", + "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", + "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", + "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", + "outside", "overlay", "overline", "padding", "padding-box", "painted", + "paused", "persian", "plus-darker", "plus-lighter", "pointer", "portrait", + "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button", + "radio", "read-only", "read-write", "read-write-plaintext-only", "relative", + "repeat", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba", + "ridge", "right", "round", "row-resize", "rtl", "run-in", "running", + "s-resize", "sans-serif", "scroll", "scrollbar", "se-resize", "searchfield", + "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", + "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", + "single", "skip-white-space", "slide", "slider-horizontal", + "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", + "small", "small-caps", "small-caption", "smaller", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "square", + "square-button", "start", "static", "status-bar", "stretch", "stroke", + "sub", "subpixel-antialiased", "super", "sw-resize", "table", + "table-caption", "table-cell", "table-column", "table-column-group", + "table-footer-group", "table-header-group", "table-row", "table-row-group", + "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", + "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", + "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", + "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", + "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", + "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", + "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", + "visibleStroke", "visual", "w-resize", "wait", "wave", "white", "wider", + "window", "windowframe", "windowtext", "x-large", "x-small", "xor", + "xx-large", "xx-small", "yellow" + ]); function keySet(array) { var keys = {}; for (var i = 0; i < array.length; ++i) keys[array[i]] = true; return keys; } function ret(style, tp) {type = tp; return style;} function tokenBase(stream, state) { var ch = stream.next(); - if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("meta", stream.current());} + if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());} else if (ch == "/" && stream.eat("*")) { state.tokenize = tokenCComment; return tokenCComment(stream, state); @@ -81,15 +217,29 @@ CodeMirror.defineMode("css", function(config) { stream.eatWhile(/[\w.%]/); return ret("number", "unit"); } - else if (/[,.+>*\/]/.test(ch)) { + else if (ch === "-") { + if (/\d/.test(stream.peek())) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (stream.match(/^[^-]+-/)) { + return ret("meta", type); + } + } + else if (/[,+>*\/]/.test(ch)) { return ret(null, "select-op"); } - else if (/[;{}:\[\]\(\)]/.test(ch)) { + else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { + return ret("qualifier", type); + } + else if (ch == ":") { + return ret("operator", ch); + } + else if (/[;{}\[\]\(\)]/.test(ch)) { return ret(null, ch); } else { stream.eatWhile(/[\w\\\-]/); - return ret("variable", "variable"); + return ret("property", "variable"); } } @@ -138,32 +288,156 @@ CodeMirror.defineMode("css", function(config) { }, token: function(stream, state) { + + // Use these terms when applicable (see http://www.xanthir.com/blog/b4E50) + // + // rule** or **ruleset: + // A selector + braces combo, or an at-rule. + // + // declaration block: + // A sequence of declarations. + // + // declaration: + // A property + colon + value combo. + // + // property value: + // The entire value of a property. + // + // component value: + // A single piece of a property value. Like the 5px in + // text-shadow: 0 0 5px blue;. Can also refer to things that are + // multiple terms, like the 1-4 terms that make up the background-size + // portion of the background shorthand. + // + // term: + // The basic unit of author-facing CSS, like a single number (5), + // dimension (5px), string ("foo"), or function. Officially defined + // by the CSS 2.1 grammar (look for the 'term' production) + // + // + // simple selector: + // A single atomic selector, like a type selector, an attr selector, a + // class selector, etc. + // + // compound selector: + // One or more simple selectors without a combinator. div.example is + // compound, div > .example is not. + // + // complex selector: + // One or more compound selectors chained with combinators. + // + // combinator: + // The parts of selectors that express relationships. There are four + // currently - the space (descendant combinator), the greater-than + // bracket (child combinator), the plus sign (next sibling combinator), + // and the tilda (following sibling combinator). + // + // sequence of selectors: + // One or more of the named type of selector chained with commas. + if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); + // Changing style returned based on context var context = state.stack[state.stack.length-1]; - if (type == "hash" && context != "rule") style = "string-2"; - else if (style == "variable") { - if (context == "rule") style = keywords[stream.current()] ? "keyword" : "number"; - else if (!context || context == "@media{") style = "tag"; + if (style == "property") { + if (context == "propertyValue"){ + if (valueKeywords[stream.current()]) { + style = "string-2"; + } else if (colorKeywords[stream.current()]) { + style = "keyword"; + } else { + style = "variable-2"; + } + } else if (context == "rule") { + if (!propertyKeywords[stream.current()]) { + style += " error"; + } + } else if (!context || context == "@media{") { + style = "tag"; + } else if (context == "@media") { + if (atMediaTypes[stream.current()]) { + style = "attribute"; // Known attribute + } else if (/^(only|not)$/i.test(stream.current())) { + style = "keyword"; + } else if (stream.current().toLowerCase() == "and") { + style = "error"; // "and" is only allowed in @mediaType + } else if (atMediaFeatures[stream.current()]) { + style = "error"; // Known property, should be in @mediaType( + } else { + // Unknown, expecting keyword or attribute, assuming attribute + style = "attribute error"; + } + } else if (context == "@mediaType") { + if (atMediaTypes[stream.current()]) { + style = "attribute"; + } else if (stream.current().toLowerCase() == "and") { + style = "operator"; + } else if (/^(only|not)$/i.test(stream.current())) { + style = "error"; // Only allowed in @media + } else if (atMediaFeatures[stream.current()]) { + style = "error"; // Known property, should be in parentheses + } else { + // Unknown attribute or property, but expecting property (preceded + // by "and"). Should be in parentheses + style = "error"; + } + } else if (context == "@mediaType(") { + if (propertyKeywords[stream.current()]) { + // do nothing, remains "property" + } else if (atMediaTypes[stream.current()]) { + style = "error"; // Known property, should be in parentheses + } else if (stream.current().toLowerCase() == "and") { + style = "operator"; + } else if (/^(only|not)$/i.test(stream.current())) { + style = "error"; // Only allowed in @media + } else { + style += " error"; + } + } else { + style = "error"; + } + } else if (style == "atom") { + if(!context || context == "@media{") { + style = "builtin"; + } else if (context == "propertyValue") { + if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) { + style += " error"; + } + } else { + style = "error"; + } + } else if (context == "@media" && type == "{") { + style = "error"; } - if (context == "rule" && /^[\{\};]$/.test(type)) - state.stack.pop(); + // Push/pop context stack if (type == "{") { - if (context == "@media") state.stack[state.stack.length-1] = "@media{"; - else state.stack.push("{"); + if (context == "@media" || context == "@mediaType") { + state.stack.pop(); + state.stack[state.stack.length-1] = "@media{"; + } + else state.stack.push("rule"); + } + else if (type == "}") { + state.stack.pop(); + if (context == "propertyValue") state.stack.pop(); } - else if (type == "}") state.stack.pop(); else if (type == "@media") state.stack.push("@media"); - else if (context == "{" && type != "comment") state.stack.push("rule"); + else if (context == "@media" && /\b(keyword|attribute)\b/.test(style)) + state.stack.push("@mediaType"); + else if (context == "@mediaType" && stream.current() == ",") state.stack.pop(); + else if (context == "@mediaType" && type == "(") state.stack.push("@mediaType("); + else if (context == "@mediaType(" && type == ")") state.stack.pop(); + else if (context == "rule" && type == ":") state.stack.push("propertyValue"); + else if (context == "propertyValue" && type == ";") state.stack.pop(); return style; }, indent: function(state, textAfter) { var n = state.stack.length; if (/^\}/.test(textAfter)) - n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1; + n -= state.stack[state.stack.length-1] == "propertyValue" ? 2 : 1; return state.baseIndent + n * indentUnit; }, diff --git a/mode/css/index.html b/mode/css/index.html index 1a591cbf3d..ae2c3bfcee 100644 --- a/mode/css/index.html +++ b/mode/css/index.html @@ -52,5 +52,7 @@

CodeMirror: CSS mode

MIME types defined: text/css.

+

Parsing/Highlighting Tests: normal, verbose.

+ diff --git a/mode/css/test.js b/mode/css/test.js new file mode 100644 index 0000000000..fd6a4b8aa8 --- /dev/null +++ b/mode/css/test.js @@ -0,0 +1,501 @@ +// Initiate ModeTest and set defaults +var MT = ModeTest; +MT.modeName = 'css'; +MT.modeOptions = {}; + +// Requires at least one media query +MT.testMode( + 'atMediaEmpty', + '@media { }', + [ + 'def', '@media', + null, ' ', + 'error', '{', + null, ' }' + ] +); + +MT.testMode( + 'atMediaMultiple', + '@media not screen and (color), not print and (color) { }', + [ + 'def', '@media', + null, ' ', + 'keyword', 'not', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'operator', 'and', + null, ' (', + 'property', 'color', + null, '), ', + 'keyword', 'not', + null, ' ', + 'attribute', 'print', + null, ' ', + 'operator', 'and', + null, ' (', + 'property', 'color', + null, ') { }' + ] +); + +MT.testMode( + 'atMediaCheckStack', + '@media screen { } foo { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' { } ', + 'tag', 'foo', + null, ' { }' + ] +); + +MT.testMode( + 'atMediaCheckStack', + '@media screen (color) { } foo { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' (', + 'property', 'color', + null, ') { } ', + 'tag', 'foo', + null, ' { }' + ] +); + +MT.testMode( + 'atMediaCheckStackInvalidAttribute', + '@media foobarhello { } foo { }', + [ + 'def', '@media', + null, ' ', + 'attribute error', 'foobarhello', + null, ' { } ', + 'tag', 'foo', + null, ' { }' + ] +); + +// Error, because "and" is only allowed immediately preceding a media expression +MT.testMode( + 'atMediaInvalidAttribute', + '@media foobarhello { }', + [ + 'def', '@media', + null, ' ', + 'attribute error', 'foobarhello', + null, ' { }' + ] +); + +// Error, because "and" is only allowed immediately preceding a media expression +MT.testMode( + 'atMediaInvalidAnd', + '@media and screen { }', + [ + 'def', '@media', + null, ' ', + 'error', 'and', + null, ' ', + 'attribute', 'screen', + null, ' { }' + ] +); + +// Error, because "not" is only allowed as the first item in each media query +MT.testMode( + 'atMediaInvalidNot', + '@media screen not (not) { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'error', 'not', + null, ' (', + 'error', 'not', + null, ') { }' + ] +); + +// Error, because "only" is only allowed as the first item in each media query +MT.testMode( + 'atMediaInvalidOnly', + '@media screen only (only) { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'error', 'only', + null, ' (', + 'error', 'only', + null, ') { }' + ] +); + +// Error, because "foobarhello" is neither a known type or property, but +// property was expected (after "and"), and it should be in parenthese. +MT.testMode( + 'atMediaUnknownType', + '@media screen and foobarhello { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'operator', 'and', + null, ' ', + 'error', 'foobarhello', + null, ' { }' + ] +); + +// Error, because "color" is not a known type, but is a known property, and +// should be in parentheses. +MT.testMode( + 'atMediaInvalidType', + '@media screen and color { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'operator', 'and', + null, ' ', + 'error', 'color', + null, ' { }' + ] +); + +// Error, because "print" is not a known property, but is a known type, +// and should not be in parenthese. +MT.testMode( + 'atMediaInvalidProperty', + '@media screen and (print) { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'operator', 'and', + null, ' (', + 'error', 'print', + null, ') { }' + ] +); + +// Soft error, because "foobarhello" is not a known property or type. +MT.testMode( + 'atMediaUnknownProperty', + '@media screen and (foobarhello) { }', + [ + 'def', '@media', + null, ' ', + 'attribute', 'screen', + null, ' ', + 'operator', 'and', + null, ' (', + 'property error', 'foobarhello', + null, ') { }' + ] +); + +MT.testMode( + 'tagSelector', + 'foo { }', + [ + 'tag', 'foo', + null, ' { }' + ] +); + +MT.testMode( + 'classSelector', + '.foo-bar_hello { }', + [ + 'qualifier', '.foo-bar_hello', + null, ' { }' + ] +); + +MT.testMode( + 'idSelector', + '#foo { #foo }', + [ + 'builtin', '#foo', + null, ' { ', + 'error', '#foo', + null, ' }' + ] +); + +MT.testMode( + 'tagSelectorUnclosed', + 'foo { margin: 0 } bar { }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'margin', + 'operator', ':', + null, ' ', + 'number', '0', + null, ' } ', + 'tag', 'bar', + null, ' { }' + ] +); + +MT.testMode( + 'tagStringNoQuotes', + 'foo { font-family: hello world; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'font-family', + 'operator', ':', + null, ' ', + 'variable-2', 'hello', + null, ' ', + 'variable-2', 'world', + null, '; }' + ] +); + +MT.testMode( + 'tagStringDouble', + 'foo { font-family: "hello world"; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'font-family', + 'operator', ':', + null, ' ', + 'string', '"hello world"', + null, '; }' + ] +); + +MT.testMode( + 'tagStringSingle', + 'foo { font-family: \'hello world\'; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'font-family', + 'operator', ':', + null, ' ', + 'string', '\'hello world\'', + null, '; }' + ] +); + +MT.testMode( + 'tagColorKeyword', + 'foo { color: black; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'color', + 'operator', ':', + null, ' ', + 'keyword', 'black', + null, '; }' + ] +); + +MT.testMode( + 'tagColorHex3', + 'foo { background: #fff; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'background', + 'operator', ':', + null, ' ', + 'atom', '#fff', + null, '; }' + ] +); + +MT.testMode( + 'tagColorHex6', + 'foo { background: #ffffff; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'background', + 'operator', ':', + null, ' ', + 'atom', '#ffffff', + null, '; }' + ] +); + +MT.testMode( + 'tagColorHex4', + 'foo { background: #ffff; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'background', + 'operator', ':', + null, ' ', + 'atom error', '#ffff', + null, '; }' + ] +); + +MT.testMode( + 'tagColorHexInvalid', + 'foo { background: #ffg; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'background', + 'operator', ':', + null, ' ', + 'atom error', '#ffg', + null, '; }' + ] +); + +MT.testMode( + 'tagNegativeNumber', + 'foo { margin: -5px; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'margin', + 'operator', ':', + null, ' ', + 'number', '-5px', + null, '; }' + ] +); + +MT.testMode( + 'tagPositiveNumber', + 'foo { padding: 5px; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'padding', + 'operator', ':', + null, ' ', + 'number', '5px', + null, '; }' + ] +); + +MT.testMode( + 'tagVendor', + 'foo { -foo-box-sizing: -foo-border-box; }', + [ + 'tag', 'foo', + null, ' { ', + 'meta', '-foo-', + 'property', 'box-sizing', + 'operator', ':', + null, ' ', + 'meta', '-foo-', + 'string-2', 'border-box', + null, '; }' + ] +); + +MT.testMode( + 'tagBogusProperty', + 'foo { barhelloworld: 0; }', + [ + 'tag', 'foo', + null, ' { ', + 'property error', 'barhelloworld', + 'operator', ':', + null, ' ', + 'number', '0', + null, '; }' + ] +); + +MT.testMode( + 'tagTwoProperties', + 'foo { margin: 0; padding: 0; }', + [ + 'tag', 'foo', + null, ' { ', + 'property', 'margin', + 'operator', ':', + null, ' ', + 'number', '0', + null, '; ', + 'property', 'padding', + 'operator', ':', + null, ' ', + 'number', '0', + null, '; }' + ] +); +// +//MT.testMode( +// 'tagClass', +// '@media only screen and (min-width: 500px), print {foo.bar#hello { color: black !important; background: #f00; margin: -5px; padding: 5px; -foo-box-sizing: border-box; } /* world */}', +// [ +// 'def', '@media', +// null, ' ', +// 'keyword', 'only', +// null, ' ', +// 'attribute', 'screen', +// null, ' ', +// 'operator', 'and', +// null, ' ', +// 'bracket', '(', +// 'property', 'min-width', +// 'operator', ':', +// null, ' ', +// 'number', '500px', +// 'bracket', ')', +// null, ', ', +// 'attribute', 'print', +// null, ' {', +// 'tag', 'foo', +// 'qualifier', '.bar', +// 'header', '#hello', +// null, ' { ', +// 'property', 'color', +// 'operator', ':', +// null, ' ', +// 'keyword', 'black', +// null, ' ', +// 'keyword', '!important', +// null, '; ', +// 'property', 'background', +// 'operator', ':', +// null, ' ', +// 'atom', '#f00', +// null, '; ', +// 'property', 'padding', +// 'operator', ':', +// null, ' ', +// 'number', '5px', +// null, '; ', +// 'property', 'margin', +// 'operator', ':', +// null, ' ', +// 'number', '-5px', +// null, '; ', +// 'meta', '-foo-', +// 'property', 'box-sizing', +// 'operator', ':', +// null, ' ', +// 'string-2', 'border-box', +// null, '; } ', +// 'comment', '/* world */', +// null, '}' +// ] +//); \ No newline at end of file diff --git a/mode/gfm/gfm.js b/mode/gfm/gfm.js index b83fbc683a..21b8259390 100644 --- a/mode/gfm/gfm.js +++ b/mode/gfm/gfm.js @@ -1,145 +1,94 @@ CodeMirror.defineMode("gfm", function(config, parserConfig) { - var mdMode = CodeMirror.getMode(config, "markdown"); - var aliases = { - html: "htmlmixed", - js: "javascript", - json: "application/json", - c: "text/x-csrc", - "c++": "text/x-c++src", - java: "text/x-java", - csharp: "text/x-csharp", - "c#": "text/x-csharp" - }; - - // make this lazy so that we don't need to load GFM last - var getMode = (function () { - var i, modes = {}, mimes = {}, mime; - - var list = CodeMirror.listModes(); - for (i = 0; i < list.length; i++) { - modes[list[i]] = list[i]; - } - var mimesList = CodeMirror.listMIMEs(); - for (i = 0; i < mimesList.length; i++) { - mime = mimesList[i].mime; - mimes[mime] = mimesList[i].mime; - } - - for (var a in aliases) { - if (aliases[a] in modes || aliases[a] in mimes) - modes[a] = aliases[a]; - } - - return function (lang) { - return modes[lang] ? CodeMirror.getMode(config, modes[lang]) : null; - }; - }()); - - function markdown(stream, state) { - // intercept fenced code blocks - if (stream.sol() && stream.match(/^```([\w+#]*)/)) { - // try switching mode - state.localMode = getMode(RegExp.$1); - if (state.localMode) - state.localState = state.localMode.startState(); - - state.token = local; - return 'code'; - } - - return mdMode.token(stream, state.mdState); - } - - function local(stream, state) { - if (stream.sol() && stream.match(/^```/)) { - state.localMode = state.localState = null; - state.token = markdown; - return 'code'; - } - else if (state.localMode) { - return state.localMode.token(stream, state.localState); - } else { - stream.skipToEnd(); - return 'code'; - } - } - - // custom handleText to prevent emphasis in the middle of a word - // and add autolinking - function handleText(stream, mdState) { - var match; - if (stream.match(/^\w+:\/\/\S+/)) { - return 'link'; - } - if (stream.match(/^[^\[*\\<>` _][^\[*\\<>` ]*[^\[*\\<>` _]/)) { - return mdMode.getType(mdState); - } - if (match = stream.match(/^[^\[*\\<>` ]+/)) { - var word = match[0]; - if (word[0] === '_' && word[word.length-1] === '_') { - stream.backUp(word.length); - return undefined; - } - return mdMode.getType(mdState); - } - if (stream.eatSpace()) { - return null; - } + var codeDepth = 0; + function blankLine(state) { + state.code = false; + return null; } - - return { + var gfmOverlay = { startState: function() { - var mdState = mdMode.startState(); - mdState.text = handleText; - return {token: markdown, mode: "markdown", mdState: mdState, - localMode: null, localState: null}; + return { + code: false, + codeBlock: false, + ateSpace: false + }; }, - - copyState: function(state) { - return {token: state.token, mode: state.mode, mdState: CodeMirror.copyState(mdMode, state.mdState), - localMode: state.localMode, - localState: state.localMode ? CodeMirror.copyState(state.localMode, state.localState) : null}; + copyState: function(s) { + return { + code: s.code, + codeBlock: s.codeBlock, + ateSpace: s.ateSpace + }; }, - token: function(stream, state) { - /* Parse GFM double bracket links */ - var ch; - if ((ch = stream.peek()) != undefined && ch == '[') { - stream.next(); // Advance the stream - - /* Only handle double bracket links */ - if ((ch = stream.peek()) == undefined || ch != '[') { - stream.backUp(1); - return state.token(stream, state); - } - - while ((ch = stream.next()) != undefined && ch != ']') {} - - if (ch == ']' && (ch = stream.next()) != undefined && ch == ']') - return 'link'; - - /* If we did not find the second ']' */ - stream.backUp(1); - } - - /* Match GFM latex formulas, as well as latex formulas within '$' */ - if (stream.match(/^\$[^\$]+\$/)) { - return "string"; + // Hack to prevent formatting override inside code blocks (block and inline) + if (state.codeBlock) { + if (stream.match(/^```/)) { + state.codeBlock = false; + return null; } - - if (stream.match(/^\\\((.*?)\\\)/)) { - return "string"; - } - - if (stream.match(/^\$\$[^\$]+\$\$/)) { - return "string"; + stream.skipToEnd(); + return null; + } + if (stream.sol()) { + state.code = false; + } + if (stream.sol() && stream.match(/^```/)) { + stream.skipToEnd(); + state.codeBlock = true; + return null; + } + // If this block is changed, it may need to be updated in Markdown mode + if (stream.peek() === '`') { + stream.next(); + var before = stream.pos; + stream.eatWhile('`'); + var difference = 1 + stream.pos - before; + if (!state.code) { + codeDepth = difference; + state.code = true; + } else { + if (difference === codeDepth) { // Must be exact + state.code = false; + } } - - if (stream.match(/^\\\[(.*?)\\\]/)) { - return "string"; + return null; + } else if (state.code) { + stream.next(); + return null; + } + // Check if space. If so, links can be formatted later on + if (stream.eatSpace()) { + state.ateSpace = true; + return null; + } + if (stream.sol() || state.ateSpace) { + state.ateSpace = false; + if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) { + // User/Project@SHA + // User@SHA + // SHA + return "link"; + } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { + // User/Project#Num + // User#Num + // #Num + return "link"; } - - return state.token(stream, state); - } + } + if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i)) { + // URLs + // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls + return "link"; + } + stream.next(); + return null; + }, + blankLine: blankLine }; -}, "markdown"); + CodeMirror.defineMIME("gfmBase", { + name: "markdown", + underscoresBreakWords: false, + fencedCodeBlocks: true + }); + return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay); +}); diff --git a/mode/gfm/index.html b/mode/gfm/index.html index d0214c17d6..05256f4be4 100644 --- a/mode/gfm/index.html +++ b/mode/gfm/index.html @@ -5,10 +5,17 @@ CodeMirror: GFM mode + + + + + + + @@ -16,14 +23,17 @@

CodeMirror: GFM mode

-
@@ -44,5 +63,9 @@

CodeMirror: GFM mode

}); +

Optionally depends on other modes for properly highlighted code blocks.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ diff --git a/mode/gfm/test.js b/mode/gfm/test.js new file mode 100644 index 0000000000..3a261f8f77 --- /dev/null +++ b/mode/gfm/test.js @@ -0,0 +1,225 @@ +// Initiate ModeTest and set defaults +var MT = ModeTest; +MT.modeName = 'gfm'; +MT.modeOptions = {}; + +// Emphasis characters within a word +MT.testMode( + 'emInWordAsterisk', + 'foo*bar*hello', + [ + null, 'foo', + 'em', '*bar*', + null, 'hello' + ] +); +MT.testMode( + 'emInWordUnderscore', + 'foo_bar_hello', + [ + null, 'foo_bar_hello' + ] +); +MT.testMode( + 'emStrongUnderscore', + '___foo___ bar', + [ + 'strong', '__', + 'emstrong', '_foo__', + 'em', '_', + null, ' bar' + ] +); + +// Fenced code blocks +MT.testMode( + 'fencedCodeBlocks', + '```\nfoo\n\n```\nbar', + [ + 'comment', '```', + 'comment', 'foo', + 'comment', '```', + null, 'bar' + ] +); +// Fenced code block mode switching +MT.testMode( + 'fencedCodeBlockModeSwitching', + '```javascript\nfoo\n\n```\nbar', + [ + 'comment', '```javascript', + 'variable', 'foo', + 'comment', '```', + null, 'bar' + ] +); + +// SHA +MT.testMode( + 'SHA', + 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 bar', + [ + null, 'foo ', + 'link', 'be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2', + null, ' bar' + ] +); +// GitHub highlights hashes 7-40 chars in length +MT.testMode( + 'shortSHA', + 'foo be6a8cc bar', + [ + null, 'foo ', + 'link', 'be6a8cc', + null, ' bar' + ] +); +// Invalid SHAs +// +// GitHub does not highlight hashes shorter than 7 chars +MT.testMode( + 'tooShortSHA', + 'foo be6a8c bar', + [ + null, 'foo be6a8c bar' + ] +); +// GitHub does not highlight hashes longer than 40 chars +MT.testMode( + 'longSHA', + 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar', + [ + null, 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar' + ] +); +MT.testMode( + 'badSHA', + 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar', + [ + null, 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar' + ] +); +// User@SHA +MT.testMode( + 'userSHA', + 'foo bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 hello', + [ + null, 'foo ', + 'link', 'bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2', + null, ' hello' + ] +); +// User/Project@SHA +MT.testMode( + 'userProjectSHA', + 'foo bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 world', + [ + null, 'foo ', + 'link', 'bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2', + null, ' world' + ] +); + +// #Num +MT.testMode( + 'num', + 'foo #1 bar', + [ + null, 'foo ', + 'link', '#1', + null, ' bar' + ] +); +// bad #Num +MT.testMode( + 'badNum', + 'foo #1bar hello', + [ + null, 'foo #1bar hello' + ] +); +// User#Num +MT.testMode( + 'userNum', + 'foo bar#1 hello', + [ + null, 'foo ', + 'link', 'bar#1', + null, ' hello' + ] +); +// User/Project#Num +MT.testMode( + 'userProjectNum', + 'foo bar/hello#1 world', + [ + null, 'foo ', + 'link', 'bar/hello#1', + null, ' world' + ] +); + +// Vanilla links +MT.testMode( + 'vanillaLink', + 'foo http://www.example.com/ bar', + [ + null, 'foo ', + 'link', 'http://www.example.com/', + null, ' bar' + ] +); +MT.testMode( + 'vanillaLinkPunctuation', + 'foo http://www.example.com/. bar', + [ + null, 'foo ', + 'link', 'http://www.example.com/', + null, '. bar' + ] +); +MT.testMode( + 'vanillaLinkExtension', + 'foo http://www.example.com/index.html bar', + [ + null, 'foo ', + 'link', 'http://www.example.com/index.html', + null, ' bar' + ] +); +// Not a link +MT.testMode( + 'notALink', + '```css\nfoo {color:black;}\n```http://www.example.com/', + [ + 'comment', '```css', + 'tag', 'foo', + null, ' {', + 'property', 'color', + 'operator', ':', + 'keyword', 'black', + null, ';}', + 'comment', '```', + 'link', 'http://www.example.com/' + ] +); +// Not a link +MT.testMode( + 'notALink', + '``foo `bar` http://www.example.com/`` hello', + [ + 'comment', '``foo `bar` http://www.example.com/``', + null, ' hello' + ] +); +// Not a link +MT.testMode( + 'notALink', + '`foo\nhttp://www.example.com/\n`foo\n\nhttp://www.example.com/', + [ + 'comment', '`foo', + 'link', 'http://www.example.com/', + 'comment', '`foo', + 'link', 'http://www.example.com/' + ] +); \ No newline at end of file diff --git a/mode/haxe/haxe.js b/mode/haxe/haxe.js index ea8bd834e6..64f4eb3ff8 100644 --- a/mode/haxe/haxe.js +++ b/mode/haxe/haxe.js @@ -421,9 +421,6 @@ CodeMirror.defineMode("haxe", function(config, parserConfig) { else if (lexical.align) return lexical.column + (closing ? 0 : 1); else return lexical.indented + (closing ? 0 : indentUnit); }, - compareStates: function(state1, state2) { - return (state1.localVars == state2.localVars) && (state1.context == state2.context); - }, electricChars: "{}" }; diff --git a/mode/htmlembedded/htmlembedded.js b/mode/htmlembedded/htmlembedded.js index a8a7e6e603..b7888689f1 100644 --- a/mode/htmlembedded/htmlembedded.js +++ b/mode/htmlembedded/htmlembedded.js @@ -58,11 +58,16 @@ CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { }; }, + electricChars: "/{}:", - electricChars: "/{}:" + innerMode: function(state) { + if (state.token == scriptingDispatch) return {state: state.scriptState, mode: scriptingMode}; + else return {state: state.htmlState, mode: htmlMixedMode}; + } }; }, "htmlmixed"); CodeMirror.defineMIME("application/x-ejs", { name: "htmlembedded", scriptingModeSpec:"javascript"}); CodeMirror.defineMIME("application/x-aspx", { name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); CodeMirror.defineMIME("application/x-jsp", { name: "htmlembedded", scriptingModeSpec:"text/x-java"}); +CodeMirror.defineMIME("application/x-erb", { name: "htmlembedded", scriptingModeSpec:"ruby"}); diff --git a/mode/htmlmixed/htmlmixed.js b/mode/htmlmixed/htmlmixed.js index 260a6d0dfb..9a15360dbd 100644 --- a/mode/htmlmixed/htmlmixed.js +++ b/mode/htmlmixed/htmlmixed.js @@ -1,4 +1,4 @@ -CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { +CodeMirror.defineMode("htmlmixed", function(config) { var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); var jsMode = CodeMirror.getMode(config, "javascript"); var cssMode = CodeMirror.getMode(config, "css"); @@ -9,12 +9,10 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { if (/^script$/i.test(state.htmlState.context.tagName)) { state.token = javascript; state.localState = jsMode.startState(htmlMode.indent(state.htmlState, "")); - state.mode = "javascript"; } else if (/^style$/i.test(state.htmlState.context.tagName)) { state.token = css; state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); - state.mode = "css"; } } return style; @@ -24,7 +22,7 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { var close = cur.search(pat), m; if (close > -1) stream.backUp(cur.length - close); else if (m = cur.match(/<\/?$/)) { - stream.backUp(cur[0].length); + stream.backUp(cur.length); if (!stream.match(pat, false)) stream.match(cur[0]); } return style; @@ -33,7 +31,6 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { if (stream.match(/^<\/\s*script\s*>/i, false)) { state.token = html; state.localState = null; - state.mode = "html"; return html(stream, state); } return maybeBackup(stream, /<\/\s*script\s*>/, @@ -43,7 +40,6 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { if (stream.match(/^<\/\s*style\s*>/i, false)) { state.token = html; state.localState = null; - state.mode = "html"; return html(stream, state); } return maybeBackup(stream, /<\/\s*style\s*>/, @@ -76,13 +72,12 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { return cssMode.indent(state.localState, textAfter); }, - compareStates: function(a, b) { - if (a.mode != b.mode) return false; - if (a.localState) return CodeMirror.Pass; - return htmlMode.compareStates(a.htmlState, b.htmlState); - }, + electricChars: "/{}:", - electricChars: "/{}:" + innerMode: function(state) { + var mode = state.token == html ? htmlMode : state.token == javascript ? jsMode : cssMode; + return {state: state.localState || state.htmlState, mode: mode}; + } }; }, "xml", "javascript", "css"); diff --git a/mode/javascript/index.html b/mode/javascript/index.html index 206df3fca1..a9fb381fdf 100644 --- a/mode/javascript/index.html +++ b/mode/javascript/index.html @@ -69,10 +69,17 @@

CodeMirror: JavaScript mode

}); -

JavaScript mode supports a single configuration - option, json, which will set the mode to expect JSON - data rather than a JavaScript program.

+

+ JavaScript mode supports a two configuration + options: +

+

-

MIME types defined: text/javascript, application/json.

+

MIME types defined: text/javascript, application/json, text/typescript, application/typescript.

diff --git a/mode/javascript/javascript.js b/mode/javascript/javascript.js index 6ece1befc9..e5c150420e 100644 --- a/mode/javascript/javascript.js +++ b/mode/javascript/javascript.js @@ -1,6 +1,9 @@ +// TODO actually recognize syntax of TypeScript constructs + CodeMirror.defineMode("javascript", function(config, parserConfig) { var indentUnit = config.indentUnit; var jsonMode = parserConfig.json; + var isTS = parserConfig.typescript; // Tokenizer @@ -8,7 +11,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { function kw(type) {return {type: type, style: "keyword"};} var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); var operator = kw("operator"), atom = {type: "atom", style: "atom"}; - return { + + var jsKeywords = { "if": A, "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "var": kw("var"), "const": kw("var"), "let": kw("var"), @@ -17,6 +21,35 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { "in": operator, "typeof": operator, "instanceof": operator, "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom }; + + // Extend the 'normal' keywords with the TypeScript language extensions + if (isTS) { + var type = {type: "variable", style: "variable-3"}; + var tsKeywords = { + // object-like things + "interface": kw("interface"), + "class": kw("class"), + "extends": kw("extends"), + "constructor": kw("constructor"), + + // scope modifiers + "public": kw("public"), + "private": kw("private"), + "protected": kw("protected"), + "static": kw("static"), + + "super": kw("super"), + + // types + "string": type, "number": type, "bool": type, "any": type + }; + + for (var attr in tsKeywords) { + jsKeywords[attr] = tsKeywords[attr]; + } + } + + return jsKeywords; }(); var isOperatorChar = /[+\-*&%=<>!?|]/; @@ -66,7 +99,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { stream.skipToEnd(); return ret("comment", "comment"); } - else if (state.reAllowed) { + else if (state.lastType == "operator" || state.lastType == "keyword c" || + /^[\[{}\(,;:]$/.test(state.lastType)) { nextUntilUnescaped(stream, "/"); stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla return ret("regexp", "string-2"); @@ -87,7 +121,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { else { stream.eatWhile(/[\w\$_]/); var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; - return (known && state.kwAllowed) ? ret(known.type, known.style, word) : + return (known && state.lastType != ".") ? ret(known.type, known.style, word) : ret("variable", "variable", word); } } @@ -175,8 +209,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { var defaultVars = {name: "this", next: {name: "arguments"}}; function pushcontext() { - if (!cx.state.context) cx.state.localVars = defaultVars; cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + cx.state.localVars = defaultVars; } function popcontext() { cx.state.localVars = cx.state.context.vars; @@ -275,19 +309,30 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (type == "}") return cont(); return pass(statement, block); } + function maybetype(type) { + if (type == ":") return cont(typedef); + return pass(); + } + function typedef(type) { + if (type == "variable"){cx.marked = "variable-3"; return cont();} + return pass(); + } function vardef1(type, value) { - if (type == "variable"){register(value); return cont(vardef2);} - return cont(); + if (type == "variable") { + register(value); + return isTS ? cont(maybetype, vardef2) : cont(vardef2); + } + return pass(); } function vardef2(type, value) { if (value == "=") return cont(expression, vardef2); if (type == ",") return cont(vardef1); } function forspec1(type) { - if (type == "var") return cont(vardef1, forspec2); - if (type == ";") return pass(forspec2); + if (type == "var") return cont(vardef1, expect(";"), forspec2); + if (type == ";") return cont(forspec2); if (type == "variable") return cont(formaybein); - return pass(forspec2); + return cont(forspec2); } function formaybein(type, value) { if (value == "in") return cont(expression); @@ -306,7 +351,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext); } function funarg(type, value) { - if (type == "variable") {register(value); return cont();} + if (type == "variable") {register(value); return isTS ? cont(maybetype) : cont();} } // Interface @@ -315,8 +360,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { startState: function(basecolumn) { return { tokenize: jsTokenBase, - reAllowed: true, - kwAllowed: true, + lastType: null, cc: [], lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), localVars: parserConfig.localVars, @@ -334,28 +378,34 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); if (type == "comment") return style; - state.reAllowed = !!(type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/)); - state.kwAllowed = type != '.'; + state.lastType = type; return parseJS(state, style, type, content, stream); }, indent: function(state, textAfter) { + if (state.tokenize == jsTokenComment) return CodeMirror.Pass; if (state.tokenize != jsTokenBase) return 0; var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; var type = lexical.type, closing = firstChar == type; - if (type == "vardef") return lexical.indented + 4; + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? 4 : 0); else if (type == "form" && firstChar == "{") return lexical.indented; - else if (type == "stat" || type == "form") return lexical.indented + indentUnit; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? indentUnit : 0); else if (lexical.info == "switch" && !closing) return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); else if (lexical.align) return lexical.column + (closing ? 0 : 1); else return lexical.indented + (closing ? 0 : indentUnit); }, - electricChars: ":{}" + electricChars: ":{}", + + jsonMode: jsonMode }; }); CodeMirror.defineMIME("text/javascript", "javascript"); CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); diff --git a/mode/javascript/typescript.html b/mode/javascript/typescript.html new file mode 100644 index 0000000000..58315e7ac7 --- /dev/null +++ b/mode/javascript/typescript.html @@ -0,0 +1,48 @@ + + + + + CodeMirror: TypeScript mode + + + + + + + +

CodeMirror: TypeScript mode

+ +
+ + + +

This is a specialization of the JavaScript mode.

+ + diff --git a/mode/lua/lua.js b/mode/lua/lua.js index 60e84a9264..97fb2c6f96 100644 --- a/mode/lua/lua.js +++ b/mode/lua/lua.js @@ -64,7 +64,7 @@ CodeMirror.defineMode("lua", function(config, parserConfig) { function normal(stream, state) { var ch = stream.next(); if (ch == "-" && stream.eat("-")) { - if (stream.eat("[")) + if (stream.eat("[") && stream.eat("[")) return (state.cur = bracketed(readBracket(stream), "comment"))(stream, state); stream.skipToEnd(); return "comment"; diff --git a/mode/markdown/index.html b/mode/markdown/index.html index 59e79f6fd0..92d5f1fb08 100644 --- a/mode/markdown/index.html +++ b/mode/markdown/index.html @@ -337,5 +337,7 @@

CodeMirror: Markdown mode

MIME types defined: text/x-markdown.

+

Parsing/Highlighting Tests: normal, verbose.

+ diff --git a/mode/markdown/markdown.js b/mode/markdown/markdown.js index 9eab617573..d227fc9b91 100644 --- a/mode/markdown/markdown.js +++ b/mode/markdown/markdown.js @@ -2,12 +2,59 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { var htmlFound = CodeMirror.mimeModes.hasOwnProperty("text/html"); var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? "text/html" : "text/plain"); + var aliases = { + html: "htmlmixed", + js: "javascript", + json: "application/json", + c: "text/x-csrc", + "c++": "text/x-c++src", + java: "text/x-java", + csharp: "text/x-csharp", + "c#": "text/x-csharp" + }; + + var getMode = (function () { + var i, modes = {}, mimes = {}, mime; + + var list = CodeMirror.listModes(); + for (i = 0; i < list.length; i++) { + modes[list[i]] = list[i]; + } + var mimesList = CodeMirror.listMIMEs(); + for (i = 0; i < mimesList.length; i++) { + mime = mimesList[i].mime; + mimes[mime] = mimesList[i].mime; + } + + for (var a in aliases) { + if (aliases[a] in modes || aliases[a] in mimes) + modes[a] = aliases[a]; + } + + return function (lang) { + return modes[lang] ? CodeMirror.getMode(cmCfg, modes[lang]) : null; + }; + }()); + + // Should underscores in words open/close em/strong? + if (modeCfg.underscoresBreakWords === undefined) + modeCfg.underscoresBreakWords = true; + + // Turn on fenced code blocks? ("```" to start/end) + if (modeCfg.fencedCodeBlocks === undefined) modeCfg.fencedCodeBlocks = false; + + var codeDepth = 0; + var prevLineHasContent = false + , thisLineHasContent = false; var header = 'header' , code = 'comment' , quote = 'quote' , list = 'string' , hr = 'hr' + , image = 'tag' + , linkinline = 'link' + , linkemail = 'link' , linktext = 'link' , linkhref = 'string' , em = 'em' @@ -17,8 +64,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { var hrRE = /^([*\-=_])(?:\s*\1){2,}\s*$/ , ulRE = /^[*\-+]\s+/ , olRE = /^[0-9]+\.\s+/ - , headerRE = /^(?:\={3,}|-{3,})$/ - , textRE = /^[^\[*_\\<>`]+/; + , headerRE = /^(?:\={1,}|-{1,})$/ + , textRE = /^[^!\[\]*_\\<>` "'(]+/; function switchInline(stream, state, f) { state.f = state.inline = f; @@ -34,6 +81,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { // Blocks function blankLine(state) { + // Reset linkTitle state + state.linkTitle = false; // Reset EM state state.em = false; // Reset STRONG state @@ -48,14 +97,23 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { } function blockNormal(stream, state) { - var match; + + if (state.list !== false && state.indentationDiff >= 0) { // Continued list + if (state.indentationDiff < 4) { // Only adjust indentation if *not* a code block + state.indentation -= state.indentationDiff; + } + state.list = null; + } else { // No longer a list + state.list = false; + } + if (state.indentationDiff >= 4) { - state.indentation -= state.indentationDiff; + state.indentation -= 4; stream.skipToEnd(); return code; } else if (stream.eatSpace()) { return null; - } else if (stream.peek() === '#' || stream.match(headerRE)) { + } else if (stream.peek() === '#' || (prevLineHasContent && stream.match(headerRE)) ) { state.header = true; } else if (stream.eat('>')) { state.indentation++; @@ -64,9 +122,15 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { return switchInline(stream, state, footnoteLink); } else if (stream.match(hrRE, true)) { return hr; - } else if (match = stream.match(ulRE, true) || stream.match(olRE, true)) { - state.indentation += match[0].length; - return list; + } else if (stream.match(ulRE, true) || stream.match(olRE, true)) { + state.indentation += 4; + state.list = true; + } else if (modeCfg.fencedCodeBlocks && stream.match(/^```([\w+#]*)/, true)) { + // try switching mode + state.localMode = getMode(RegExp.$1); + if (state.localMode) state.localState = state.localMode.startState(); + switchBlock(stream, state, local); + return code; } return switchInline(stream, state, state.inline); @@ -86,6 +150,30 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { return style; } + function local(stream, state) { + if (stream.sol() && stream.match(/^```/, true)) { + state.localMode = state.localState = null; + state.f = inlineNormal; + state.block = blockNormal; + return code; + } else if (state.localMode) { + return state.localMode.token(stream, state.localState); + } else { + stream.skipToEnd(); + return code; + } + } + + function codeBlock(stream, state) { + if(stream.match(codeBlockRE, true)){ + state.f = inlineNormal; + state.block = blockNormal; + switchInline(stream, state, state.inline); + return code; + } + stream.skipToEnd(); + return code; + } // Inline function getType(state) { @@ -94,8 +182,13 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { if (state.strong) { styles.push(state.em ? emstrong : strong); } else if (state.em) { styles.push(em); } + if (state.linkText) { styles.push(linktext); } + + if (state.code) { styles.push(code); } + if (state.header) { styles.push(header); } if (state.quote) { styles.push(quote); } + if (state.list !== false) { styles.push(list); } return styles.length ? styles.join(' ') : null; } @@ -112,18 +205,79 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { if (typeof style !== 'undefined') return style; + if (state.list) { // List marker (*, +, -, 1., etc) + state.list = null; + return list; + } + var ch = stream.next(); if (ch === '\\') { stream.next(); return getType(state); } + + // Matches link titles present on next line + if (state.linkTitle) { + state.linkTitle = false; + var matchCh = ch; + if (ch === '(') { + matchCh = ')'; + } + matchCh = (matchCh+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh; + if (stream.match(new RegExp(regex), true)) { + return linkhref; + } + } + + // If this block is changed, it may need to be updated in GFM mode if (ch === '`') { - return switchInline(stream, state, inlineElement(code, '`')); + var t = getType(state); + var before = stream.pos; + stream.eatWhile('`'); + var difference = 1 + stream.pos - before; + if (!state.code) { + codeDepth = difference; + state.code = true; + return getType(state); + } else { + if (difference === codeDepth) { // Must be exact + state.code = false; + return t; + } + return getType(state); + } + } else if (state.code) { + return getType(state); + } + + if (ch === '!' && stream.match(/\[.*\] ?(?:\(|\[)/, false)) { + stream.match(/\[.*\]/); + state.inline = state.f = linkHref; + return image; + } + + if (ch === '[' && stream.match(/.*\](\(| ?\[)/, false)) { + state.linkText = true; + return getType(state); } - if (ch === '[' && stream.match(/.*\](?:\(|\[)/, false)) { - return switchInline(stream, state, linkText); + + if (ch === ']' && state.linkText) { + var type = getType(state); + state.linkText = false; + state.inline = state.f = linkHref; + return type; + } + + if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, true)) { + return switchInline(stream, state, inlineElement(linkinline, '>')); } + + if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, true)) { + return switchInline(stream, state, inlineElement(linkemail, '>')); + } + if (ch === '<' && stream.match(/^\w/, false)) { var md_inside = false; if (stream.string.indexOf(">")!=-1) { @@ -141,31 +295,51 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { return "tag"; } + var ignoreUnderscore = false; + if (!modeCfg.underscoresBreakWords) { + if (ch === '_' && stream.peek() !== '_' && stream.match(/(\w)/, false)) { + var prevPos = stream.pos - 2; + if (prevPos >= 0) { + var prevCh = stream.string.charAt(prevPos); + if (prevCh !== '_' && prevCh.match(/(\w)/, false)) { + ignoreUnderscore = true; + } + } + } + } var t = getType(state); - if (ch === '*' || ch === '_') { - if (stream.eat(ch)) { - return (state.strong = !state.strong) ? getType(state) : t; + if (ch === '*' || (ch === '_' && !ignoreUnderscore)) { + if (state.strong === ch && stream.eat(ch)) { // Remove STRONG + state.strong = false; + return t; + } else if (!state.strong && stream.eat(ch)) { // Add STRONG + state.strong = ch; + return getType(state); + } else if (state.em === ch) { // Remove EM + state.em = false; + return t; + } else if (!state.em) { // Add EM + state.em = ch; + return getType(state); + } + } else if (ch === ' ') { + if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces + if (stream.peek() === ' ') { // Surrounded by spaces, ignore + return getType(state); + } else { // Not surrounded by spaces, back up pointer + stream.backUp(1); + } } - return (state.em = !state.em) ? getType(state) : t; } return getType(state); } - function linkText(stream, state) { - while (!stream.eol()) { - var ch = stream.next(); - if (ch === '\\') stream.next(); - if (ch === ']') { - state.inline = state.f = linkHref; - return linktext; - } - } - return linktext; - } - function linkHref(stream, state) { - stream.eatSpace(); + // Check if space, and return NULL if so (to avoid marking the space) + if(stream.eatSpace()){ + return null; + } var ch = stream.next(); if (ch === '(' || ch === '[') { return switchInline(stream, state, inlineElement(linkhref, ch === '(' ? ')' : ']')); @@ -182,19 +356,32 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { } function footnoteUrl(stream, state) { - stream.eatSpace(); + // Check if space, and return NULL if so (to avoid marking the space) + if(stream.eatSpace()){ + return null; + } + // Match URL stream.match(/^[^\s]+/, true); + // Check for link title + if (stream.peek() === undefined) { // End of line, set flag to check next line + state.linkTitle = true; + } else { // More content on line, check if link title + stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true); + } state.f = state.inline = inlineNormal; return linkhref; } + var savedInlineRE = []; function inlineRE(endChar) { - if (!inlineRE[endChar]) { - // match any not-escaped-non-endChar and any escaped char - // then match endChar or eol - inlineRE[endChar] = new RegExp('^(?:[^\\\\\\' + endChar + ']|\\\\.)*(?:\\' + endChar + '|$)'); + if (!savedInlineRE[endChar]) { + // Escape endChar for RegExp (taken from http://stackoverflow.com/a/494122/526741) + endChar = (endChar+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + // Match any non-endChar, escaped character, as well as the closing + // endChar. + savedInlineRE[endChar] = new RegExp('^(?:[^\\\\]|\\\\.)*?(' + endChar + ')'); } - return inlineRE[endChar]; + return savedInlineRE[endChar]; } function inlineElement(type, endChar, next) { @@ -208,6 +395,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { return { startState: function() { + prevLineHasContent = false; + thisLineHasContent = false; return { f: blockNormal, @@ -217,9 +406,13 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { inline: inlineNormal, text: handleText, + + linkText: false, + linkTitle: false, em: false, strong: false, header: false, + list: false, quote: false }; }, @@ -231,12 +424,17 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { block: s.block, htmlState: CodeMirror.copyState(htmlMode, s.htmlState), indentation: s.indentation, + + localMode: s.localMode, + localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null, inline: s.inline, text: s.text, + linkTitle: s.linkTitle, em: s.em, strong: s.strong, header: s.header, + list: s.list, quote: s.quote, md_inside: s.md_inside }; @@ -244,13 +442,28 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { token: function(stream, state) { if (stream.sol()) { - if (stream.match(/^\s*$/, true)) { return blankLine(state); } + if (stream.match(/^\s*$/, true)) { + prevLineHasContent = false; + return blankLine(state); + } else { + if(thisLineHasContent){ + prevLineHasContent = true; + thisLineHasContent = false; + } + thisLineHasContent = true; + } // Reset state.header state.header = false; + + // Reset state.code + state.code = false; state.f = state.block; var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length; + var difference = Math.floor((indentation - state.indentation) / 4) * 4; + if (difference > 4) difference = 4; + indentation = state.indentation + difference; state.indentationDiff = indentation - state.indentation; state.indentation = indentation; if (indentation > 0) { return null; } diff --git a/mode/markdown/test.js b/mode/markdown/test.js new file mode 100644 index 0000000000..7572db510b --- /dev/null +++ b/mode/markdown/test.js @@ -0,0 +1,1266 @@ +// Initiate ModeTest and set defaults +var MT = ModeTest; +MT.modeName = 'markdown'; +MT.modeOptions = {}; + +MT.testMode( + 'plainText', + 'foo', + [ + null, 'foo' + ] +); + +// Code blocks using 4 spaces (regardless of CodeMirror.tabSize value) +MT.testMode( + 'codeBlocksUsing4Spaces', + ' foo', + [ + null, ' ', + 'comment', 'foo' + ] +); +// Code blocks using 4 spaces with internal indentation +MT.testMode( + 'codeBlocksUsing4SpacesIndentation', + ' bar\n hello\n world\n foo\nbar', + [ + null, ' ', + 'comment', 'bar', + null, ' ', + 'comment', 'hello', + null, ' ', + 'comment', 'world', + null, ' ', + 'comment', 'foo', + null, 'bar' + ] +); +// Code blocks using 4 spaces with internal indentation +MT.testMode( + 'codeBlocksUsing4SpacesIndentation', + ' foo\n bar\n hello\n world', + [ + null, ' foo', + null, ' ', + 'comment', 'bar', + null, ' ', + 'comment', 'hello', + null, ' ', + 'comment', 'world' + ] +); + +// Code blocks using 1 tab (regardless of CodeMirror.indentWithTabs value) +MT.testMode( + 'codeBlocksUsing1Tab', + '\tfoo', + [ + null, '\t', + 'comment', 'foo' + ] +); + +// Inline code using backticks +MT.testMode( + 'inlineCodeUsingBackticks', + 'foo `bar`', + [ + null, 'foo ', + 'comment', '`bar`' + ] +); + +// Block code using single backtick (shouldn't work) +MT.testMode( + 'blockCodeSingleBacktick', + '`\nfoo\n`', + [ + 'comment', '`', + null, 'foo', + 'comment', '`' + ] +); + +// Unclosed backticks +// Instead of simply marking as CODE, it would be nice to have an +// incomplete flag for CODE, that is styled slightly different. +MT.testMode( + 'unclosedBackticks', + 'foo `bar', + [ + null, 'foo ', + 'comment', '`bar' + ] +); + +// Per documentation: "To include a literal backtick character within a +// code span, you can use multiple backticks as the opening and closing +// delimiters" +MT.testMode( + 'doubleBackticks', + '``foo ` bar``', + [ + 'comment', '``foo ` bar``' + ] +); + +// Tests based on Dingus +// http://daringfireball.net/projects/markdown/dingus +// +// Multiple backticks within an inline code block +MT.testMode( + 'consecutiveBackticks', + '`foo```bar`', + [ + 'comment', '`foo```bar`' + ] +); +// Multiple backticks within an inline code block with a second code block +MT.testMode( + 'consecutiveBackticks', + '`foo```bar` hello `world`', + [ + 'comment', '`foo```bar`', + null, ' hello ', + 'comment', '`world`' + ] +); +// Unclosed with several different groups of backticks +MT.testMode( + 'unclosedBackticks', + '``foo ``` bar` hello', + [ + 'comment', '``foo ``` bar` hello' + ] +); +// Closed with several different groups of backticks +MT.testMode( + 'closedBackticks', + '``foo ``` bar` hello`` world', + [ + 'comment', '``foo ``` bar` hello``', + null, ' world' + ] +); + +// atx headers +// http://daringfireball.net/projects/markdown/syntax#header +// +// H1 +MT.testMode( + 'atxH1', + '# foo', + [ + 'header', '# foo' + ] +); +// H2 +MT.testMode( + 'atxH2', + '## foo', + [ + 'header', '## foo' + ] +); +// H3 +MT.testMode( + 'atxH3', + '### foo', + [ + 'header', '### foo' + ] +); +// H4 +MT.testMode( + 'atxH4', + '#### foo', + [ + 'header', '#### foo' + ] +); +// H5 +MT.testMode( + 'atxH5', + '##### foo', + [ + 'header', '##### foo' + ] +); +// H6 +MT.testMode( + 'atxH6', + '###### foo', + [ + 'header', '###### foo' + ] +); +// H6 - 7x '#' should still be H6, per Dingus +// http://daringfireball.net/projects/markdown/dingus +MT.testMode( + 'atxH6NotH7', + '####### foo', + [ + 'header', '####### foo' + ] +); + +// Setext headers - H1, H2 +// Per documentation, "Any number of underlining =’s or -’s will work." +// http://daringfireball.net/projects/markdown/syntax#header +// Ideally, the text would be marked as `header` as well, but this is +// not really feasible at the moment. So, instead, we're testing against +// what works today, to avoid any regressions. +// +// Check if single underlining = works +MT.testMode( + 'setextH1', + 'foo\n=', + [ + null, 'foo', + 'header', '=' + ] +); +// Check if 3+ ='s work +MT.testMode( + 'setextH1', + 'foo\n===', + [ + null, 'foo', + 'header', '===' + ] +); +// Check if single underlining - works +MT.testMode( + 'setextH2', + 'foo\n-', + [ + null, 'foo', + 'header', '-' + ] +); +// Check if 3+ -'s work +MT.testMode( + 'setextH2', + 'foo\n---', + [ + null, 'foo', + 'header', '---' + ] +); + +// Single-line blockquote with trailing space +MT.testMode( + 'blockquoteSpace', + '> foo', + [ + 'quote', '> foo' + ] +); + +// Single-line blockquote +MT.testMode( + 'blockquoteNoSpace', + '>foo', + [ + 'quote', '>foo' + ] +); + +// Single-line blockquote followed by normal paragraph +MT.testMode( + 'blockquoteThenParagraph', + '>foo\n\nbar', + [ + 'quote', '>foo', + null, 'bar' + ] +); + +// Multi-line blockquote (lazy mode) +MT.testMode( + 'multiBlockquoteLazy', + '>foo\nbar', + [ + 'quote', '>foo', + 'quote', 'bar' + ] +); + +// Multi-line blockquote followed by normal paragraph (lazy mode) +MT.testMode( + 'multiBlockquoteLazyThenParagraph', + '>foo\nbar\n\nhello', + [ + 'quote', '>foo', + 'quote', 'bar', + null, 'hello' + ] +); + +// Multi-line blockquote (non-lazy mode) +MT.testMode( + 'multiBlockquote', + '>foo\n>bar', + [ + 'quote', '>foo', + 'quote', '>bar' + ] +); + +// Multi-line blockquote followed by normal paragraph (non-lazy mode) +MT.testMode( + 'multiBlockquoteThenParagraph', + '>foo\n>bar\n\nhello', + [ + 'quote', '>foo', + 'quote', '>bar', + null, 'hello' + ] +); + +// Check list types +MT.testMode( + 'listAsterisk', + '* foo\n* bar', + [ + 'string', '* foo', + 'string', '* bar' + ] +); +MT.testMode( + 'listPlus', + '+ foo\n+ bar', + [ + 'string', '+ foo', + 'string', '+ bar' + ] +); +MT.testMode( + 'listDash', + '- foo\n- bar', + [ + 'string', '- foo', + 'string', '- bar' + ] +); +MT.testMode( + 'listNumber', + '1. foo\n2. bar', + [ + 'string', '1. foo', + 'string', '2. bar' + ] +); + +// Formatting in lists (*) +MT.testMode( + 'listAsteriskFormatting', + '* *foo* bar\n\n* **foo** bar\n\n* ***foo*** bar\n\n* `foo` bar', + [ + 'string', '* ', + 'string em', '*foo*', + 'string', ' bar', + 'string', '* ', + 'string strong', '**foo**', + 'string', ' bar', + 'string', '* ', + 'string strong', '**', + 'string emstrong', '*foo**', + 'string em', '*', + 'string', ' bar', + 'string', '* ', + 'string comment', '`foo`', + 'string', ' bar' + ] +); +// Formatting in lists (+) +MT.testMode( + 'listPlusFormatting', + '+ *foo* bar\n\n+ **foo** bar\n\n+ ***foo*** bar\n\n+ `foo` bar', + [ + 'string', '+ ', + 'string em', '*foo*', + 'string', ' bar', + 'string', '+ ', + 'string strong', '**foo**', + 'string', ' bar', + 'string', '+ ', + 'string strong', '**', + 'string emstrong', '*foo**', + 'string em', '*', + 'string', ' bar', + 'string', '+ ', + 'string comment', '`foo`', + 'string', ' bar' + ] +); +// Formatting in lists (-) +MT.testMode( + 'listDashFormatting', + '- *foo* bar\n\n- **foo** bar\n\n- ***foo*** bar\n\n- `foo` bar', + [ + 'string', '- ', + 'string em', '*foo*', + 'string', ' bar', + 'string', '- ', + 'string strong', '**foo**', + 'string', ' bar', + 'string', '- ', + 'string strong', '**', + 'string emstrong', '*foo**', + 'string em', '*', + 'string', ' bar', + 'string', '- ', + 'string comment', '`foo`', + 'string', ' bar' + ] +); +// Formatting in lists (1.) +MT.testMode( + 'listNumberFormatting', + '1. *foo* bar\n\n2. **foo** bar\n\n3. ***foo*** bar\n\n4. `foo` bar', + [ + 'string', '1. ', + 'string em', '*foo*', + 'string', ' bar', + 'string', '2. ', + 'string strong', '**foo**', + 'string', ' bar', + 'string', '3. ', + 'string strong', '**', + 'string emstrong', '*foo**', + 'string em', '*', + 'string', ' bar', + 'string', '4. ', + 'string comment', '`foo`', + 'string', ' bar' + ] +); + +// Paragraph lists +MT.testMode( + 'listParagraph', + '* foo\n\n* bar', + [ + 'string', '* foo', + 'string', '* bar' + ] +); + +// Multi-paragraph lists +// +// 4 spaces +MT.testMode( + 'listMultiParagraph', + '* foo\n\n* bar\n\n hello', + [ + 'string', '* foo', + 'string', '* bar', + null, ' ', + 'string', 'hello' + ] +); +// 4 spaces, extra blank lines (should still be list, per Dingus) +MT.testMode( + 'listMultiParagraphExtra', + '* foo\n\n* bar\n\n\n hello', + [ + 'string', '* foo', + 'string', '* bar', + null, ' ', + 'string', 'hello' + ] +); +// 4 spaces, plus 1 space (should still be list, per Dingus) +MT.testMode( + 'listMultiParagraphExtraSpace', + '* foo\n\n* bar\n\n hello\n\n world', + [ + 'string', '* foo', + 'string', '* bar', + null, ' ', + 'string', 'hello', + null, ' ', + 'string', 'world' + ] +); +// 1 tab +MT.testMode( + 'listTab', + '* foo\n\n* bar\n\n\thello', + [ + 'string', '* foo', + 'string', '* bar', + null, '\t', + 'string', 'hello' + ] +); +// No indent +MT.testMode( + 'listNoIndent', + '* foo\n\n* bar\n\nhello', + [ + 'string', '* foo', + 'string', '* bar', + null, 'hello' + ] +); +// Blockquote +MT.testMode( + 'blockquote', + '* foo\n\n* bar\n\n > hello', + [ + 'string', '* foo', + 'string', '* bar', + null, ' ', + 'string quote', '> hello' + ] +); +// Code block +MT.testMode( + 'blockquoteCode', + '* foo\n\n* bar\n\n > hello\n\n world', + [ + 'string', '* foo', + 'string', '* bar', + null, ' ', + 'comment', '> hello', + null, ' ', + 'string', 'world' + ] +); +// Code block followed by text +MT.testMode( + 'blockquoteCodeText', + '* foo\n\n bar\n\n hello\n\n world', + [ + 'string', '* foo', + null, ' ', + 'string', 'bar', + null, ' ', + 'comment', 'hello', + null, ' ', + 'string', 'world' + ] +); + +// Nested list +// +// * +MT.testMode( + 'listAsteriskNested', + '* foo\n\n * bar', + [ + 'string', '* foo', + null, ' ', + 'string', '* bar' + ] +); +// + +MT.testMode( + 'listPlusNested', + '+ foo\n\n + bar', + [ + 'string', '+ foo', + null, ' ', + 'string', '+ bar' + ] +); +// - +MT.testMode( + 'listDashNested', + '- foo\n\n - bar', + [ + 'string', '- foo', + null, ' ', + 'string', '- bar' + ] +); +// 1. +MT.testMode( + 'listNumberNested', + '1. foo\n\n 2. bar', + [ + 'string', '1. foo', + null, ' ', + 'string', '2. bar' + ] +); +// Mixed +MT.testMode( + 'listMixed', + '* foo\n\n + bar\n\n - hello\n\n 1. world', + [ + 'string', '* foo', + null, ' ', + 'string', '+ bar', + null, ' ', + 'string', '- hello', + null, ' ', + 'string', '1. world' + ] +); +// Blockquote +MT.testMode( + 'listBlockquote', + '* foo\n\n + bar\n\n > hello', + [ + 'string', '* foo', + null, ' ', + 'string', '+ bar', + null, ' ', + 'quote string', '> hello' + ] +); +// Code +MT.testMode( + 'listCode', + '* foo\n\n + bar\n\n hello', + [ + 'string', '* foo', + null, ' ', + 'string', '+ bar', + null, ' ', + 'comment', 'hello' + ] +); +// Code with internal indentation +MT.testMode( + 'listCodeIndentation', + '* foo\n\n bar\n hello\n world\n foo\n bar', + [ + 'string', '* foo', + null, ' ', + 'comment', 'bar', + null, ' ', + 'comment', 'hello', + null, ' ', + 'comment', 'world', + null, ' ', + 'comment', 'foo', + null, ' ', + 'string', 'bar' + ] +); +// Code followed by text +MT.testMode( + 'listCodeText', + '* foo\n\n bar\n\nhello', + [ + 'string', '* foo', + null, ' ', + 'comment', 'bar', + null, 'hello' + ] +); + +// Following tests directly from official Markdown documentation +// http://daringfireball.net/projects/markdown/syntax#hr +MT.testMode( + 'hrSpace', + '* * *', + [ + 'hr', '* * *' + ] +); + +MT.testMode( + 'hr', + '***', + [ + 'hr', '***' + ] +); + +MT.testMode( + 'hrLong', + '*****', + [ + 'hr', '*****' + ] +); + +MT.testMode( + 'hrSpaceDash', + '- - -', + [ + 'hr', '- - -' + ] +); + +MT.testMode( + 'hrDashLong', + '---------------------------------------', + [ + 'hr', '---------------------------------------' + ] +); + +// Inline link with title +MT.testMode( + 'linkTitle', + '[foo](http://example.com/ "bar") hello', + [ + 'link', '[foo]', + 'string', '(http://example.com/ "bar")', + null, ' hello' + ] +); + +// Inline link without title +MT.testMode( + 'linkNoTitle', + '[foo](http://example.com/) bar', + [ + 'link', '[foo]', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Inline link with Em +MT.testMode( + 'linkEm', + '[*foo*](http://example.com/) bar', + [ + 'link', '[', + 'link em', '*foo*', + 'link', ']', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Inline link with Strong +MT.testMode( + 'linkStrong', + '[**foo**](http://example.com/) bar', + [ + 'link', '[', + 'link strong', '**foo**', + 'link', ']', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Inline link with EmStrong +MT.testMode( + 'linkEmStrong', + '[***foo***](http://example.com/) bar', + [ + 'link', '[', + 'link strong', '**', + 'link emstrong', '*foo**', + 'link em', '*', + 'link', ']', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Image with title +MT.testMode( + 'imageTitle', + '![foo](http://example.com/ "bar") hello', + [ + 'tag', '![foo]', + 'string', '(http://example.com/ "bar")', + null, ' hello' + ] +); + +// Image without title +MT.testMode( + 'imageNoTitle', + '![foo](http://example.com/) bar', + [ + 'tag', '![foo]', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Image with asterisks +MT.testMode( + 'imageAsterisks', + '![*foo*](http://example.com/) bar', + [ + 'tag', '![*foo*]', + 'string', '(http://example.com/)', + null, ' bar' + ] +); + +// Not a link. Should be normal text due to square brackets being used +// regularly in text, especially in quoted material, and no space is allowed +// between square brackets and parentheses (per Dingus). +MT.testMode( + 'notALink', + '[foo] (bar)', + [ + null, '[foo] (bar)' + ] +); + +// Reference-style links +MT.testMode( + 'linkReference', + '[foo][bar] hello', + [ + 'link', '[foo]', + 'string', '[bar]', + null, ' hello' + ] +); +// Reference-style links with Em +MT.testMode( + 'linkReferenceEm', + '[*foo*][bar] hello', + [ + 'link', '[', + 'link em', '*foo*', + 'link', ']', + 'string', '[bar]', + null, ' hello' + ] +); +// Reference-style links with Strong +MT.testMode( + 'linkReferenceStrong', + '[**foo**][bar] hello', + [ + 'link', '[', + 'link strong', '**foo**', + 'link', ']', + 'string', '[bar]', + null, ' hello' + ] +); +// Reference-style links with EmStrong +MT.testMode( + 'linkReferenceEmStrong', + '[***foo***][bar] hello', + [ + 'link', '[', + 'link strong', '**', + 'link emstrong', '*foo**', + 'link em', '*', + 'link', ']', + 'string', '[bar]', + null, ' hello' + ] +); + +// Reference-style links with optional space separator (per docuentation) +// "You can optionally use a space to separate the sets of brackets" +MT.testMode( + 'linkReferenceSpace', + '[foo] [bar] hello', + [ + 'link', '[foo]', + null, ' ', + 'string', '[bar]', + null, ' hello' + ] +); +// Should only allow a single space ("...use *a* space...") +MT.testMode( + 'linkReferenceDoubleSpace', + '[foo] [bar] hello', + [ + null, '[foo] [bar] hello' + ] +); + +// Reference-style links with implicit link name +MT.testMode( + 'linkImplicit', + '[foo][] hello', + [ + 'link', '[foo]', + 'string', '[]', + null, ' hello' + ] +); + +// @todo It would be nice if, at some point, the document was actually +// checked to see if the referenced link exists + +// Link label, for reference-style links (taken from documentation) +// +// No title +MT.testMode( + 'labelNoTitle', + '[foo]: http://example.com/', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/' + ] +); +// Space in ID and title +MT.testMode( + 'labelSpaceTitle', + '[foo bar]: http://example.com/ "hello"', + [ + 'link', '[foo bar]:', + null, ' ', + 'string', 'http://example.com/ "hello"' + ] +); +// Double title +MT.testMode( + 'labelDoubleTitle', + '[foo bar]: http://example.com/ "hello" "world"', + [ + 'link', '[foo bar]:', + null, ' ', + 'string', 'http://example.com/ "hello"', + null, ' "world"' + ] +); +// Double quotes around title +MT.testMode( + 'labelTitleDoubleQuotes', + '[foo]: http://example.com/ "bar"', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/ "bar"' + ] +); +// Single quotes around title +MT.testMode( + 'labelTitleSingleQuotes', + '[foo]: http://example.com/ \'bar\'', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/ \'bar\'' + ] +); +// Parentheses around title +MT.testMode( + 'labelTitleParenthese', + '[foo]: http://example.com/ (bar)', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/ (bar)' + ] +); +// Invalid title +MT.testMode( + 'labelTitleInvalid', + '[foo]: http://example.com/ bar', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/', + null, ' bar' + ] +); +// Angle brackets around URL +MT.testMode( + 'labelLinkAngleBrackets', + '[foo]: "bar"', + [ + 'link', '[foo]:', + null, ' ', + 'string', ' "bar"' + ] +); +// Title on next line per documentation (double quotes) +MT.testMode( + 'labelTitleNextDoubleQuotes', + '[foo]: http://example.com/\n"bar" hello', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/', + 'string', '"bar"', + null, ' hello' + ] +); +// Title on next line per documentation (single quotes) +MT.testMode( + 'labelTitleNextSingleQuotes', + '[foo]: http://example.com/\n\'bar\' hello', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/', + 'string', '\'bar\'', + null, ' hello' + ] +); +// Title on next line per documentation (parentheses) +MT.testMode( + 'labelTitleNextParenthese', + '[foo]: http://example.com/\n(bar) hello', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/', + 'string', '(bar)', + null, ' hello' + ] +); +// Title on next line per documentation (mixed) +MT.testMode( + 'labelTitleNextMixed', + '[foo]: http://example.com/\n(bar" hello', + [ + 'link', '[foo]:', + null, ' ', + 'string', 'http://example.com/', + null, '(bar" hello' + ] +); + +// Automatic links +MT.testMode( + 'linkWeb', + ' foo', + [ + 'link', '', + null, ' foo' + ] +); + +// Automatic email links +MT.testMode( + 'linkEmail', + ' foo', + [ + 'link', '', + null, ' foo' + ] +); + +// Single asterisk +MT.testMode( + 'emAsterisk', + '*foo* bar', + [ + 'em', '*foo*', + null, ' bar' + ] +); + +// Single underscore +MT.testMode( + 'emUnderscore', + '_foo_ bar', + [ + 'em', '_foo_', + null, ' bar' + ] +); + +// Emphasis characters within a word +MT.testMode( + 'emInWordAsterisk', + 'foo*bar*hello', + [ + null, 'foo', + 'em', '*bar*', + null, 'hello' + ] +); +MT.testMode( + 'emInWordUnderscore', + 'foo_bar_hello', + [ + null, 'foo', + 'em', '_bar_', + null, 'hello' + ] +); +// Per documentation: "...surround an * or _ with spaces, it’ll be +// treated as a literal asterisk or underscore." +// +// Inside EM +MT.testMode( + 'emEscapedBySpaceIn', + 'foo _bar _ hello_ world', + [ + null, 'foo ', + 'em', '_bar _ hello_', + null, ' world' + ] +); +// Outside EM +MT.testMode( + 'emEscapedBySpaceOut', + 'foo _ bar_hello_world', + [ + null, 'foo _ bar', + 'em', '_hello_', + null, 'world' + ] +); + +// Unclosed emphasis characters +// Instead of simply marking as EM / STRONG, it would be nice to have an +// incomplete flag for EM and STRONG, that is styled slightly different. +MT.testMode( + 'emIncompleteAsterisk', + 'foo *bar', + [ + null, 'foo ', + 'em', '*bar' + ] +); +MT.testMode( + 'emIncompleteUnderscore', + 'foo _bar', + [ + null, 'foo ', + 'em', '_bar' + ] +); + +// Double asterisk +MT.testMode( + 'strongAsterisk', + '**foo** bar', + [ + 'strong', '**foo**', + null, ' bar' + ] +); + +// Double underscore +MT.testMode( + 'strongUnderscore', + '__foo__ bar', + [ + 'strong', '__foo__', + null, ' bar' + ] +); + +// Triple asterisk +MT.testMode( + 'emStrongAsterisk', + '*foo**bar*hello** world', + [ + 'em', '*foo', + 'emstrong', '**bar*', + 'strong', 'hello**', + null, ' world' + ] +); + +// Triple underscore +MT.testMode( + 'emStrongUnderscore', + '_foo__bar_hello__ world', + [ + 'em', '_foo', + 'emstrong', '__bar_', + 'strong', 'hello__', + null, ' world' + ] +); + +// Triple mixed +// "...same character must be used to open and close an emphasis span."" +MT.testMode( + 'emStrongMixed', + '_foo**bar*hello__ world', + [ + 'em', '_foo', + 'emstrong', '**bar*hello__ world' + ] +); + +MT.testMode( + 'emStrongMixed', + '*foo__bar_hello** world', + [ + 'em', '*foo', + 'emstrong', '__bar_hello** world' + ] +); + +// These characters should be escaped: +// \ backslash +// ` backtick +// * asterisk +// _ underscore +// {} curly braces +// [] square brackets +// () parentheses +// # hash mark +// + plus sign +// - minus sign (hyphen) +// . dot +// ! exclamation mark +// +// Backtick (code) +MT.testMode( + 'escapeBacktick', + 'foo \\`bar\\`', + [ + null, 'foo \\`bar\\`' + ] +); +MT.testMode( + 'doubleEscapeBacktick', + 'foo \\\\`bar\\\\`', + [ + null, 'foo \\\\', + 'comment', '`bar\\\\`' + ] +); +// Asterisk (em) +MT.testMode( + 'escapeAsterisk', + 'foo \\*bar\\*', + [ + null, 'foo \\*bar\\*' + ] +); +MT.testMode( + 'doubleEscapeAsterisk', + 'foo \\\\*bar\\\\*', + [ + null, 'foo \\\\', + 'em', '*bar\\\\*' + ] +); +// Underscore (em) +MT.testMode( + 'escapeUnderscore', + 'foo \\_bar\\_', + [ + null, 'foo \\_bar\\_' + ] +); +MT.testMode( + 'doubleEscapeUnderscore', + 'foo \\\\_bar\\\\_', + [ + null, 'foo \\\\', + 'em', '_bar\\\\_' + ] +); +// Hash mark (headers) +MT.testMode( + 'escapeHash', + '\\# foo', + [ + null, '\\# foo' + ] +); +MT.testMode( + 'doubleEscapeHash', + '\\\\# foo', + [ + null, '\\\\# foo' + ] +); diff --git a/mode/mysql/mysql.js b/mode/mysql/mysql.js index 3098d77576..02249195b0 100644 --- a/mode/mysql/mysql.js +++ b/mode/mysql/mysql.js @@ -56,12 +56,13 @@ CodeMirror.defineMode("mysql", function(config) { curPunc = ch; return null; } - else if (ch == "-") { - var ch2 = stream.next(); - if (ch2=="-") { - stream.skipToEnd(); - return "comment"; - } + else if (ch == "-" && stream.eat("-")) { + stream.skipToEnd(); + return "comment"; + } + else if (ch == "/" && stream.eat("*")) { + state.tokenize = tokenComment; + return state.tokenize(stream, state); } else if (operatorChars.test(ch)) { stream.eatWhile(operatorChars); @@ -115,6 +116,22 @@ CodeMirror.defineMode("mysql", function(config) { }; } + function tokenComment(stream, state) { + for (;;) { + if (stream.skipTo("*")) { + stream.next(); + if (stream.eat("/")) { + state.tokenize = tokenBase; + break; + } + } else { + stream.skipToEnd(); + break; + } + } + return "comment"; + } + function pushContext(state, type, col) { state.context = {prev: state.context, indent: state.indent, col: col, type: type}; diff --git a/mode/php/php.js b/mode/php/php.js index dbe774fa59..b94317c749 100644 --- a/mode/php/php.js +++ b/mode/php/php.js @@ -56,14 +56,13 @@ var phpMode = CodeMirror.getMode(config, phpConfig); function dispatch(stream, state) { // TODO open PHP inside text/css - var isPHP = state.mode == "php"; + var isPHP = state.curMode == phpMode; if (stream.sol() && state.pending != '"') state.pending = null; if (state.curMode == htmlMode) { if (stream.match(/^<\?\w*/)) { state.curMode = phpMode; state.curState = state.php; state.curClose = "?>"; - state.mode = "php"; return "meta"; } if (state.pending == '"') { @@ -86,13 +85,11 @@ state.curMode = jsMode; state.curState = jsMode.startState(htmlMode.indent(state.curState, "")); state.curClose = /^<\/\s*script\s*>/i; - state.mode = "javascript"; } else if (/^style$/i.test(state.curState.context.tagName)) { state.curMode = cssMode; state.curState = cssMode.startState(htmlMode.indent(state.curState, "")); state.curClose = /^<\/\s*style\s*>/i; - state.mode = "css"; } } return style; @@ -101,7 +98,6 @@ state.curMode = htmlMode; state.curState = state.html; state.curClose = null; - state.mode = "html"; if (isPHP) return "meta"; else return dispatch(stream, state); } else { @@ -141,7 +137,9 @@ return state.curMode.indent(state.curState, textAfter); }, - electricChars: "/{}:" + electricChars: "/{}:", + + innerMode: function(state) { return {state: state.curState, mode: state.curMode}; } }; }, "xml", "clike", "javascript", "css"); CodeMirror.defineMIME("application/x-httpd-php", "php"); diff --git a/mode/python/python.js b/mode/python/python.js index fc5b9551c2..ff8d1003d1 100644 --- a/mode/python/python.js +++ b/mode/python/python.js @@ -160,7 +160,7 @@ CodeMirror.defineMode("python", function(conf, parserConf) { var singleline = delimiter.length == 1; var OUTCLASS = 'string'; - return function tokenString(stream, state) { + function tokenString(stream, state) { while (!stream.eol()) { stream.eatWhile(/[^'"\\]/); if (stream.eat('\\')) { @@ -183,7 +183,9 @@ CodeMirror.defineMode("python", function(conf, parserConf) { } } return OUTCLASS; - }; + } + tokenString.isString = true; + return tokenString; } function indent(stream, state, type) { @@ -325,7 +327,7 @@ CodeMirror.defineMode("python", function(conf, parserConf) { indent: function(state, textAfter) { if (state.tokenize != tokenBase) { - return 0; + return state.tokenize.isString ? CodeMirror.Pass : 0; } return state.scopes[0].offset; diff --git a/mode/shell/shell.js b/mode/shell/shell.js index 1ad898303e..d4eba852ba 100644 --- a/mode/shell/shell.js +++ b/mode/shell/shell.js @@ -60,7 +60,7 @@ CodeMirror.defineMode('shell', function(config) { stream.eatWhile(/\w/); var cur = stream.current(); if (stream.peek() === '=' && /\w+/.test(cur)) return 'def'; - return words[cur] || null; + return words.hasOwnProperty(cur) ? words[cur] : null; } function tokenString(quote) { diff --git a/mode/stex/index.html b/mode/stex/index.html index 39dc0c2436..2dafe69816 100644 --- a/mode/stex/index.html +++ b/mode/stex/index.html @@ -92,5 +92,7 @@

CodeMirror: sTeX mode

MIME types defined: text/x-stex.

+

Parsing/Highlighting Tests: normal, verbose.

+ diff --git a/mode/stex/test.html b/mode/stex/test.html deleted file mode 100644 index 599e592dde..0000000000 --- a/mode/stex/test.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - CodeMirror: sTeX mode - - - - - - - - -

Tests for the sTeX Mode

-

Basics

- - -

Tags

- - -

Comments

- - -

Errors

- - -

Character Escapes

- - -

Spacing control

- - - -

New Commands

- - Should be able to define a new command that happens to be a method on Array - (e.g. pop): - - -

Summary

- - - - - diff --git a/mode/stex/test.js b/mode/stex/test.js new file mode 100644 index 0000000000..c5a34f3d8d --- /dev/null +++ b/mode/stex/test.js @@ -0,0 +1,343 @@ +var MT = ModeTest; +MT.modeName = 'stex'; +MT.modeOptions = {}; + +MT.testMode( + 'word', + 'foo', + [ + null, 'foo' + ] +); + +MT.testMode( + 'twoWords', + 'foo bar', + [ + null, 'foo bar' + ] +); + +MT.testMode( + 'beginEndDocument', + '\\begin{document}\n\\end{document}', + [ + 'tag', '\\begin', + 'bracket', '{', + 'atom', 'document', + 'bracket', '}', + 'tag', '\\end', + 'bracket', '{', + 'atom', 'document', + 'bracket', '}' + ] +); + +MT.testMode( + 'beginEndEquation', + '\\begin{equation}\n E=mc^2\n\\end{equation}', + [ + 'tag', '\\begin', + 'bracket', '{', + 'atom', 'equation', + 'bracket', '}', + null, ' E=mc^2', + 'tag', '\\end', + 'bracket', '{', + 'atom', 'equation', + 'bracket', '}' + ] +); + +MT.testMode( + 'beginModule', + '\\begin{module}[]', + [ + 'tag', '\\begin', + 'bracket', '{', + 'atom', 'module', + 'bracket', '}[]' + ] +); + +MT.testMode( + 'beginModuleId', + '\\begin{module}[id=bbt-size]', + [ + 'tag', '\\begin', + 'bracket', '{', + 'atom', 'module', + 'bracket', '}[', + null, 'id=bbt-size', + 'bracket', ']' + ] +); + +MT.testMode( + 'importModule', + '\\importmodule[b-b-t]{b-b-t}', + [ + 'tag', '\\importmodule', + 'bracket', '[', + 'string', 'b-b-t', + 'bracket', ']{', + 'builtin', 'b-b-t', + 'bracket', '}' + ] +); + +MT.testMode( + 'importModulePath', + '\\importmodule[\\KWARCslides{dmath/en/cardinality}]{card}', + [ + 'tag', '\\importmodule', + 'bracket', '[', + 'tag', '\\KWARCslides', + 'bracket', '{', + 'string', 'dmath/en/cardinality', + 'bracket', '}]{', + 'builtin', 'card', + 'bracket', '}' + ] +); + +MT.testMode( + 'psForPDF', + '\\PSforPDF[1]{#1}', // could treat #1 specially + [ + 'tag', '\\PSforPDF', + 'bracket', '[', + 'atom', '1', + 'bracket', ']{', + null, '#1', + 'bracket', '}' + ] +); + +MT.testMode( + 'comment', + '% foo', + [ + 'comment', '% foo' + ] +); + +MT.testMode( + 'tagComment', + '\\item% bar', + [ + 'tag', '\\item', + 'comment', '% bar' + ] +); + +MT.testMode( + 'commentTag', + ' % \\item', + [ + null, ' ', + 'comment', '% \\item' + ] +); + +MT.testMode( + 'commentLineBreak', + '%\nfoo', + [ + 'comment', '%', + null, 'foo' + ] +); + +MT.testMode( + 'tagErrorCurly', + '\\begin}{', + [ + 'tag', '\\begin', + 'error', '}', + 'bracket', '{' + ] +); + +MT.testMode( + 'tagErrorSquare', + '\\item]{', + [ + 'tag', '\\item', + 'error', ']', + 'bracket', '{' + ] +); + +MT.testMode( + 'commentCurly', + '% }', + [ + 'comment', '% }' + ] +); + +MT.testMode( + 'tagHash', + 'the \\# key', + [ + null, 'the ', + 'tag', '\\#', + null, ' key' + ] +); + +MT.testMode( + 'tagNumber', + 'a \\$5 stetson', + [ + null, 'a ', + 'tag', '\\$', + 'atom', 5, + null, ' stetson' + ] +); + +MT.testMode( + 'tagPercent', + '100\\% beef', + [ + 'atom', '100', + 'tag', '\\%', + null, ' beef' + ] +); + +MT.testMode( + 'tagAmpersand', + 'L \\& N', + [ + null, 'L ', + 'tag', '\\&', + null, ' N' + ] +); + +MT.testMode( + 'tagUnderscore', + 'foo\\_bar', + [ + null, 'foo', + 'tag', '\\_', + null, 'bar' + ] +); + +MT.testMode( + 'tagBracketOpen', + '\\emph{\\{}', + [ + 'tag', '\\emph', + 'bracket', '{', + 'tag', '\\{', + 'bracket', '}' + ] +); + +MT.testMode( + 'tagBracketClose', + '\\emph{\\}}', + [ + 'tag', '\\emph', + 'bracket', '{', + 'tag', '\\}', + 'bracket', '}' + ] +); + +MT.testMode( + 'tagLetterNumber', + 'section \\S1', + [ + null, 'section ', + 'tag', '\\S', + 'atom', '1' + ] +); + +MT.testMode( + 'textTagNumber', + 'para \\P2', + [ + null, 'para ', + 'tag', '\\P', + 'atom', '2' + ] +); + +MT.testMode( + 'thinspace', + 'x\\,y', // thinspace + [ + null, 'x', + 'tag', '\\,', + null, 'y' + ] +); + +MT.testMode( + 'thickspace', + 'x\\;y', // thickspace + [ + null, 'x', + 'tag', '\\;', + null, 'y' + ] +); + +MT.testMode( + 'negativeThinspace', + 'x\\!y', // negative thinspace + [ + null, 'x', + 'tag', '\\!', + null, 'y' + ] +); + +MT.testMode( + 'periodNotSentence', + 'J.\\ L.\\ is', // period not ending a sentence + [ + null, 'J.\\ L.\\ is' + ] +); // maybe could be better + +MT.testMode( + 'periodSentence', + 'X\\@. The', // period ending a sentence + [ + null, 'X', + 'tag', '\\@', + null, '. The' + ] +); + +MT.testMode( + 'italicCorrection', + '{\\em If\\/} I', // italic correction + [ + 'bracket', '{', + 'tag', '\\em', + null, ' If', + 'tag', '\\/', + 'bracket', '}', + null, ' I' + ] +); + +MT.testMode( + 'tagBracket', + '\\newcommand{\\pop}', + [ + 'tag', '\\newcommand', + 'bracket', '{', + 'tag', '\\pop', + 'bracket', '}' + ] +); \ No newline at end of file diff --git a/mode/tiki/tiki.js b/mode/tiki/tiki.js index 24bf0fbfe5..af83dc1b5b 100644 --- a/mode/tiki/tiki.js +++ b/mode/tiki/tiki.js @@ -301,13 +301,6 @@ CodeMirror.defineMode('tiki', function(config, parserConfig) { if (context) return context.indent + indentUnit; else return 0; }, - compareStates: function(a, b) { - if (a.indented != b.indented || a.pluginName != b.pluginName) return false; - for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) { - if (!ca || !cb) return ca == cb; - if (ca.pluginName != cb.pluginName) return false; - } - }, electricChars: "/" }; }); diff --git a/mode/vb/vb.js b/mode/vb/vb.js index 01f9890389..be01d13ad9 100644 --- a/mode/vb/vb.js +++ b/mode/vb/vb.js @@ -12,8 +12,8 @@ CodeMirror.defineMode("vb", function(conf, parserConf) { var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); - var openingKeywords = ['class','module', 'sub','enum','select','while','if','function', 'get','set','property']; - var middleKeywords = ['else','elseif','case']; + var openingKeywords = ['class','module', 'sub','enum','select','while','if','function', 'get','set','property', 'try']; + var middleKeywords = ['else','elseif','case', 'catch']; var endKeywords = ['next','loop']; var wordOperators = wordRegexp(['and', 'or', 'not', 'xor', 'in']); diff --git a/mode/xml/index.html b/mode/xml/index.html index 9628d954c0..49a627de73 100644 --- a/mode/xml/index.html +++ b/mode/xml/index.html @@ -6,7 +6,7 @@ - + diff --git a/mode/xml/xml.js b/mode/xml/xml.js index cd69f62fd5..de3656c297 100644 --- a/mode/xml/xml.js +++ b/mode/xml/xml.js @@ -114,7 +114,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { return state.tokenize(stream, state); } else { - stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/); + stream.eatWhile(/[^\s\u00a0=<>\"\']/); return "word"; } } @@ -255,6 +255,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { function attribute(type) { if (type == "equals") return cont(attvalue, attributes); if (!Kludges.allowMissing) setStyle = "error"; + else if (type == "word") setStyle = "attribute"; return (type == "endTag" || type == "selfcloseTag") ? pass() : cont(); } function attvalue(type) { @@ -308,14 +309,6 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { else return 0; }, - compareStates: function(a, b) { - if (a.indented != b.indented || a.tokenize != b.tokenize) return false; - for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) { - if (!ca || !cb) return ca == cb; - if (ca.tagName != cb.tagName || ca.indent != cb.indent) return false; - } - }, - electricChars: "/" }; }); diff --git a/mode/z80/index.html b/mode/z80/index.html new file mode 100644 index 0000000000..133c870e32 --- /dev/null +++ b/mode/z80/index.html @@ -0,0 +1,39 @@ + + + + + CodeMirror: Z80 assembly mode + + + + + + + +

CodeMirror: Z80 assembly mode

+ +
+ + + +

MIME type defined: text/x-z80.

+ + diff --git a/mode/z80/z80.js b/mode/z80/z80.js new file mode 100644 index 0000000000..c026790dc7 --- /dev/null +++ b/mode/z80/z80.js @@ -0,0 +1,113 @@ +CodeMirror.defineMode('z80', function() +{ + var keywords1 = /^(exx?|(ld|cp|in)([di]r?)?|pop|push|ad[cd]|cpl|daa|dec|inc|neg|sbc|sub|and|bit|[cs]cf|x?or|res|set|r[lr]c?a?|r[lr]d|s[lr]a|srl|djnz|nop|rst|[de]i|halt|im|ot[di]r|out[di]?)\b/i; + var keywords2 = /^(call|j[pr]|ret[in]?)\b/i; + var keywords3 = /^b_?(call|jump)\b/i; + var variables1 = /^(af?|bc?|c|de?|e|hl?|l|i[xy]?|r|sp)\b/i; + var variables2 = /^(n?[zc]|p[oe]?|m)\b/i; + var errors = /^([hl][xy]|i[xy][hl]|slia|sll)\b/i; + var numbers = /^([\da-f]+h|[0-7]+o|[01]+b|\d+)\b/i; + + return {startState: function() + { + return {context: 0}; + }, token: function(stream, state) + { + if (!stream.column()) + state.context = 0; + + if (stream.eatSpace()) + return null; + + var w; + + if (stream.eatWhile(/\w/)) + { + w = stream.current(); + + if (stream.indentation()) + { + if (state.context == 1 && variables1.test(w)) + return 'variable-2'; + + if (state.context == 2 && variables2.test(w)) + return 'variable-3'; + + if (keywords1.test(w)) + { + state.context = 1; + return 'keyword'; + } + else if (keywords2.test(w)) + { + state.context = 2; + return 'keyword'; + } + else if (keywords3.test(w)) + { + state.context = 3; + return 'keyword'; + } + + if (errors.test(w)) + return 'error'; + } + else if (numbers.test(w)) + { + return 'number'; + } + else + { + return null; + } + } + else if (stream.eat(';')) + { + stream.skipToEnd(); + return 'comment'; + } + else if (stream.eat('"')) + { + while (w = stream.next()) + { + if (w == '"') + break; + + if (w == '\\') + stream.next(); + } + + return 'string'; + } + else if (stream.eat('\'')) + { + if (stream.match(/\\?.'/)) + return 'number'; + } + else if (stream.eat('.') || stream.sol() && stream.eat('#')) + { + state.context = 4; + + if (stream.eatWhile(/\w/)) + return 'def'; + } + else if (stream.eat('$')) + { + if (stream.eatWhile(/[\da-f]/i)) + return 'number'; + } + else if (stream.eat('%')) + { + if (stream.eatWhile(/[01]/)) + return 'number'; + } + else + { + stream.next(); + } + + return null; + }}; +}); + +CodeMirror.defineMIME("text/x-z80", "z80"); diff --git a/package.json b/package.json index 8658354fc0..c16512cb7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "CodeMirror", - "version":"2.33.0", + "name": "codemirror", + "version":"2.35.1", "main": "codemirror.js", "description": "In-browser code editing made bearable", "licenses": [{"type": "MIT", diff --git a/test/driver.js b/test/driver.js index 975c24f9fc..934cb936c9 100644 --- a/test/driver.js +++ b/test/driver.js @@ -1,35 +1,111 @@ -var tests = [], debug = null; +var tests = [], debug = null, debugUsed = new Array(), allNames = []; function Failure(why) {this.message = why;} +function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; +} + function test(name, run, expectedFail) { + // Force unique names + var originalName = name; + var i = 2; // Second function would be NAME_2 + while (indexOf(allNames, name) !== -1){ + name = originalName + "_" + i; + i++; + } + allNames.push(name); + // Add test tests.push({name: name, func: run, expectedFail: expectedFail}); return name; } function testCM(name, run, opts, expectedFail) { - return test(name, function() { + return test("core_" + name, function() { var place = document.getElementById("testground"), cm = CodeMirror(place, opts); - if (debug) place.style.visibility = ""; - try {run(cm);} - finally {if (!debug) place.removeChild(cm.getWrapperElement());} + var successful = false; + try { + run(cm); + successful = true; + } finally { + if ((debug && !successful) || verbose) { + place.style.visibility = ""; + } else { + place.removeChild(cm.getWrapperElement()); + } + } }, expectedFail); } function runTests(callback) { + if (debug) { + if (indexOf(debug, "verbose") === 0) { + verbose = true; + debug.splice(0, 1); + } + if (debug.length < 1) { + debug = null; + } else { + if (totalTests > debug.length) { + totalTests = debug.length; + } + } + } + var totalTime = 0; function step(i) { - if (i == tests.length) return callback("done"); - var test = tests[i], expFail = test.expectedFail; - if (debug != null && debug != test.name) return step(i + 1); + if (i === tests.length){ + running = false; + return callback("done"); + } + var test = tests[i], expFail = test.expectedFail, startTime = +new Date; + if (debug !== null) { + var debugIndex = indexOf(debug, test.name); + if (debugIndex !== -1) { + // Remove from array for reporting incorrect tests later + debug.splice(debugIndex, 1); + } else { + var wildcardName = test.name.split("_").shift() + "_*"; + debugIndex = indexOf(debug, wildcardName); + if (debugIndex !== -1) { + // Remove from array for reporting incorrect tests later + debug.splice(debugIndex, 1); + debugUsed.push(wildcardName); + } else { + debugIndex = indexOf(debugUsed, wildcardName); + if (debugIndex !== -1) { + totalTests++; + } else { + return step(i + 1); + } + } + } + } try { - test.func(); - if (expFail) callback("fail", test.name, "was expected to fail, but succeeded"); - else callback("ok", test.name); + var message = test.func(); + if (expFail) callback("fail", test.name, message); + else callback("ok", test.name, message); } catch(e) { if (expFail) callback("expected", test.name); else if (e instanceof Failure) callback("fail", test.name, e.message); - else callback("error", test.name, e.toString()); + else { + var pos = /\bat .*?([^\/:]+):(\d+):/.exec(e.stack); + callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : "")); + } + } + if (!quit) { // Run next test + var delay = 0; + totalTime += (+new Date) - startTime; + if (totalTime > 500){ + totalTime = 0; + delay = 50; + } + setTimeout(function(){step(i + 1);}, delay); + } else { // Quit tests + running = false; + return null; } - setTimeout(function(){step(i + 1);}, 20); } step(0); } diff --git a/test/index.html b/test/index.html index dd319e9db7..043cadea31 100644 --- a/test/index.html +++ b/test/index.html @@ -1,9 +1,13 @@ + CodeMirror: Test Suite + + + @@ -11,6 +15,14 @@ .ok {color: #090;} .fail {color: #e00;} .error {color: #c90;} + .done {font-weight: bold;} + #progress { + background: #45d; + color: white; + text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d; + font-weight: bold; + white-space: pre; + } @@ -19,42 +31,139 @@

CodeMirror: Test Suite

A limited set of programmatic sanity tests for CodeMirror.

-
+
Ran 0 of 0 tests
-

+    

Please enable JavaScript...

+
- + + + + + + + + + + diff --git a/test/mode_test.css b/test/mode_test.css index f425922e5d..1ac66737fb 100644 --- a/test/mode_test.css +++ b/test/mode_test.css @@ -8,15 +8,3 @@ .mt-output .mt-style { font-size: x-small; } - -.mt-test { - border-left: 10px solid #fff; -} - -.mt-pass { - border-left: 10px solid #cfc; -} - -.mt-fail { - border-left: 10px solid #fcc; -} diff --git a/test/mode_test.js b/test/mode_test.js index d77ac143f8..8d9df65e6c 100644 --- a/test/mode_test.js +++ b/test/mode_test.js @@ -10,33 +10,49 @@ ModeTest.modeOptions = {}; ModeTest.modeName = CodeMirror.defaults.mode; /* keep track of results for printSummary */ -ModeTest.tests = 0; +ModeTest.testCount = 0; ModeTest.passes = 0; /** * Run a test; prettyprints the results using document.write(). - * - * @param string to highlight - * - * @param style[i] expected style of the i'th token in string - * - * @param token[i] expected value for the i'th token in string + * + * @param name Name of test + * @param text String to highlight. + * @param expected Expected styles and tokens: Array(style, token, [style, token,...]) + * @param modeName + * @param modeOptions + * @param expectedFail */ -ModeTest.test = function() { - ModeTest.tests += 1; +ModeTest.testMode = function(name, text, expected, modeName, modeOptions, expectedFail) { + ModeTest.testCount += 1; + + if (!modeName) modeName = ModeTest.modeName; + + if (!modeOptions) modeOptions = ModeTest.modeOptions; - var mode = CodeMirror.getMode(ModeTest.modeOptions, ModeTest.modeName); + var mode = CodeMirror.getMode(modeOptions, modeName); - if (arguments.length < 1) { - throw "must have text for test"; + if (expected.length < 0) { + throw "must have text for test (" + name + ")"; } - if (arguments.length % 2 != 1) { - throw "must have text for test plus expected (style, token) pairs"; + if (expected.length % 2 != 0) { + throw "must have text for test (" + name + ") plus expected (style, token) pairs"; } + return test( + modeName + "_" + name, + function(){ + return ModeTest.compare(text, expected, mode); + }, + expectedFail + ); + +} + +ModeTest.compare = function (text, arguments, mode) { - var text = arguments[0]; var expectedOutput = []; - for (var i = 1; i < arguments.length; i += 2) { + for (var i = 0; i < arguments.length; i += 2) { + arguments[i] = (arguments[i] != null ? arguments[i].split(' ').sort().join(' ') : arguments[i]); expectedOutput.push([arguments[i],arguments[i + 1]]); } @@ -50,20 +66,26 @@ ModeTest.test = function() { } var s = ''; - s += '
'; - s += '
' + ModeTest.htmlEscape(text) + '
'; - s += '
'; if (pass || expectedOutput.length == 0) { + s += '
'; + s += '
' + ModeTest.htmlEscape(text) + '
'; + s += '
'; s += ModeTest.prettyPrintOutputTable(observedOutput); + s += '
'; + s += '
'; + return s; } else { + s += '
'; + s += '
' + ModeTest.htmlEscape(text) + '
'; + s += '
'; s += 'expected:'; s += ModeTest.prettyPrintOutputTable(expectedOutput); s += 'observed:'; s += ModeTest.prettyPrintOutputTable(observedOutput); + s += '
'; + s += '
'; + throw s; } - s += '
'; - s += '
'; - document.write(s); } /** @@ -85,11 +107,27 @@ ModeTest.highlight = function(string, mode) { var line = lines[i]; var stream = new CodeMirror.StringStream(line); if (line == "" && mode.blankLine) mode.blankLine(state); + var pos = 0; + var st = []; + /* Start copied code from CodeMirror.highlight */ while (!stream.eol()) { - var style = mode.token(stream, state); - var substr = line.slice(stream.start, stream.pos); - output.push([style, substr]); + var style = mode.token(stream, state), substr = stream.current(); stream.start = stream.pos; + if (pos && st[pos-1] == style) { + st[pos-2] += substr; + } else if (substr) { + st[pos++] = substr; st[pos++] = style; + } + // Give up when line is ridiculously long + if (stream.pos > 5000) { + st[pos++] = this.text.slice(stream.pos); st[pos++] = null; + break; + } + } + /* End copied code from CodeMirror.highlight */ + for (var x = 0; x < st.length; x += 2) { + st[x + 1] = (st[x + 1] != null ? st[x + 1].split(' ').sort().join(' ') : st[x + 1]); + output.push([st[x + 1], st[x]]); } } @@ -131,7 +169,7 @@ ModeTest.prettyPrintOutputTable = function(output) { var token = output[i]; s += '' + - '' + + '' + ModeTest.htmlEscape(token[1]).replace(/ /g,'·') + '' + ''; @@ -150,7 +188,8 @@ ModeTest.prettyPrintOutputTable = function(output) { * Print how many tests have run so far and how many of those passed. */ ModeTest.printSummary = function() { - document.write(ModeTest.passes + ' passes for ' + ModeTest.tests + ' tests'); + ModeTest.runTests(ModeTest.displayTest); + document.write(ModeTest.passes + ' passes for ' + ModeTest.testCount + ' tests'); } /** diff --git a/test/phantom_driver.js b/test/phantom_driver.js index ad48fd1a84..e5072946c6 100644 --- a/test/phantom_driver.js +++ b/test/phantom_driver.js @@ -7,14 +7,14 @@ page.open("http://localhost:3000/test/index.html", function (status) { } waitFor(function () { return page.evaluate(function () { - var output = document.getElementById('output'); + var output = document.getElementById('status'); if (!output) { return false; } - return (/(\d+ failures?|all passed)$/i).test(output.innerText); + return (/^(\d+ failures?|all passed)/i).test(output.innerText); }); }, function () { var failed = page.evaluate(function () { return window.failed; }); var output = page.evaluate(function () { - return document.getElementById('output').innerText; + return document.getElementById('status').innerText; }); console.log(output); phantom.exit(failed > 0 ? 1 : 0); @@ -27,4 +27,4 @@ function waitFor (test, cb) { } else { setTimeout(function () { waitFor(test, cb); }, 250); } -} \ No newline at end of file +} diff --git a/test/test.js b/test/test.js index 2c8e398c83..9fb4016a4b 100644 --- a/test/test.js +++ b/test/test.js @@ -24,7 +24,7 @@ function byClassName(elt, cls) { var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); -test("fromTextArea", function() { +test("core_fromTextArea", function() { var te = document.getElementById("code"); te.value = "CONTENT"; var cm = CodeMirror.fromTextArea(te); @@ -108,7 +108,7 @@ testCM("indent", function(cm) { eq(cm.getLine(1), "\t\t blah();"); }, {value: "if (x) {\nblah();\n}", indentUnit: 3, indentWithTabs: true, tabSize: 8}); -test("defaults", function() { +test("core_defaults", function() { var olddefaults = CodeMirror.defaults, defs = CodeMirror.defaults = {}; for (var opt in olddefaults) defs[opt] = olddefaults[opt]; defs.indentUnit = 5; @@ -257,13 +257,14 @@ testCM("markTextSingleLine", function(cm) { var r = cm.markText({line: 0, ch: 3}, {line: 0, ch: 6}, "foo"); cm.replaceRange(test.c, {line: 0, ch: test.a}, {line: 0, ch: test.b}); var f = r.find(); - eq(f.from && f.from.ch, test.f); eq(f.to && f.to.ch, test.t); + eq(f && f.from.ch, test.f); eq(f && f.to.ch, test.t); }); }); testCM("markTextMultiLine", function(cm) { function p(v) { return v && {line: v[0], ch: v[1]}; } forEach([{a: [0, 0], b: [0, 5], c: "", f: [0, 0], t: [2, 5]}, + {a: [0, 0], b: [0, 5], c: "foo\n", f: [1, 0], t: [3, 5]}, {a: [0, 1], b: [0, 10], c: "", f: [0, 1], t: [2, 5]}, {a: [0, 5], b: [0, 6], c: "x", f: [0, 6], t: [2, 5]}, {a: [0, 0], b: [1, 0], c: "", f: [0, 0], t: [1, 5]}, @@ -275,16 +276,38 @@ testCM("markTextMultiLine", function(cm) { {a: [1, 5], b: [2, 5], c: "", f: [0, 5], t: [1, 5]}, {a: [2, 0], b: [2, 3], c: "", f: [0, 5], t: [2, 2]}, {a: [2, 5], b: [3, 0], c: "a\nb", f: [0, 5], t: [2, 5]}, - {a: [2, 3], b: [3, 0], c: "x", f: [0, 5], t: [2, 4]}, + {a: [2, 3], b: [3, 0], c: "x", f: [0, 5], t: [2, 3]}, {a: [1, 1], b: [1, 9], c: "1\n2\n3", f: [0, 5], t: [4, 5]}], function(test) { cm.setValue("aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddd\n"); - var r = cm.markText({line: 0, ch: 5}, {line: 2, ch: 5}, "foo"); + var r = cm.markText({line: 0, ch: 5}, {line: 2, ch: 5}, "CodeMirror-matchingbracket"); cm.replaceRange(test.c, p(test.a), p(test.b)); var f = r.find(); - eqPos(f.from, p(test.f)); eqPos(f.to, p(test.t)); + eqPos(f && f.from, p(test.f)); eqPos(f && f.to, p(test.t)); }); }); +testCM("markTextUndo", function(cm) { + var marker1, marker2, bookmark; + cm.compoundChange(function(){ + marker1 = cm.markText({line: 0, ch: 1}, {line: 0, ch: 3}, "CodeMirror-matchingbracket"); + marker2 = cm.markText({line: 0, ch: 0}, {line: 2, ch: 1}, "CodeMirror-matchingbracket"); + bookmark = cm.setBookmark({line: 1, ch: 5}); + }); + cm.compoundChange(function(){ + cm.replaceRange("foo", {line: 0, ch: 2}); + cm.replaceRange("bar\baz\bug\n", {line: 2, ch: 0}, {line: 3, ch: 0}); + }); + cm.setValue(""); + eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null); + cm.undo(); + eqPos(bookmark.find(), {line: 1, ch: 5}); + cm.undo(); + var m1Pos = marker1.find(), m2Pos = marker2.find(); + eqPos(m1Pos.from, {line: 0, ch: 1}); eqPos(m1Pos.to, {line: 0, ch: 3}); + eqPos(m2Pos.from, {line: 0, ch: 0}); eqPos(m2Pos.to, {line: 2, ch: 1}); + eqPos(bookmark.find(), {line: 1, ch: 5}); +}, {value: "1234\n56789\n00\n"}); + testCM("markClearBetween", function(cm) { cm.setValue("aaa\nbbb\nccc\nddd\n"); cm.markText({line: 0, ch: 0}, {line: 2}, "foo"); diff --git a/theme/ambiance-mobile.css b/theme/ambiance-mobile.css new file mode 100644 index 0000000000..d60d19bd33 --- /dev/null +++ b/theme/ambiance-mobile.css @@ -0,0 +1,6 @@ +.CodeMirror .cm-s-ambiance { + -webkit-box-shadow: none; + -moz-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; +} diff --git a/theme/ambiance.css b/theme/ambiance.css index ef5f8d09ba..3b93f9c7a5 100644 --- a/theme/ambiance.css +++ b/theme/ambiance.css @@ -1,4 +1,4 @@ -/* ambiance theme for code-mirror */ +/* ambiance theme for codemirror */ /* Color scheme */