Owen Leonard
698efcc0aa
This patch adds CodeMirror syntax highlighting by default to JS and CSS system preferences. HTML preferences will use CodeMirror editors if UseWYSIWYGinSystemPreferences is disabled. Three new CodeMirror files are added to support three new syntax highlighting modes: XML (for HTML), CSS, and JS. A new option is added to *.pref file configurations for textareas which are intended for HTML, JS, or CSS: syntax. This option is passed to the CodeMirror configuration to control syntax highlighting mode. Textareas without a syntax option specified will not have CodeMirror enabled. To test, apply the patch and go to Administration -> System preferences. Test the behavior of several preferences which use <textarea> as their input. For example: - OPACUserJS (JS) - IntranetUserCSS (CSS) - OpacHeader (HTML) - BibtexExportAdditionalFields (no highlighting) Text entry in each of these should have the correct syntax highlighting applied to them. All data should be saved correctly. Test with UseWYSIWYGinSystemPreferences both on and off. Signed-off-by: Liz Rea <wizzyrea@gmail.com> Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
402 lines
13 KiB
JavaScript
402 lines
13 KiB
JavaScript
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
|
|
|
(function(mod) {
|
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
mod(require("../../lib/codemirror"));
|
|
else if (typeof define == "function" && define.amd) // AMD
|
|
define(["../../lib/codemirror"], mod);
|
|
else // Plain browser env
|
|
mod(CodeMirror);
|
|
})(function(CodeMirror) {
|
|
"use strict";
|
|
|
|
var htmlConfig = {
|
|
autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
|
|
'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
|
|
'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
|
|
'track': true, 'wbr': true, 'menuitem': true},
|
|
implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
|
|
'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
|
|
'th': true, 'tr': true},
|
|
contextGrabbers: {
|
|
'dd': {'dd': true, 'dt': true},
|
|
'dt': {'dd': true, 'dt': true},
|
|
'li': {'li': true},
|
|
'option': {'option': true, 'optgroup': true},
|
|
'optgroup': {'optgroup': true},
|
|
'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
|
|
'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
|
|
'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
|
|
'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
|
|
'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
|
|
'rp': {'rp': true, 'rt': true},
|
|
'rt': {'rp': true, 'rt': true},
|
|
'tbody': {'tbody': true, 'tfoot': true},
|
|
'td': {'td': true, 'th': true},
|
|
'tfoot': {'tbody': true},
|
|
'th': {'td': true, 'th': true},
|
|
'thead': {'tbody': true, 'tfoot': true},
|
|
'tr': {'tr': true}
|
|
},
|
|
doNotIndent: {"pre": true},
|
|
allowUnquoted: true,
|
|
allowMissing: true,
|
|
caseFold: true
|
|
}
|
|
|
|
var xmlConfig = {
|
|
autoSelfClosers: {},
|
|
implicitlyClosed: {},
|
|
contextGrabbers: {},
|
|
doNotIndent: {},
|
|
allowUnquoted: false,
|
|
allowMissing: false,
|
|
allowMissingTagName: false,
|
|
caseFold: false
|
|
}
|
|
|
|
CodeMirror.defineMode("xml", function(editorConf, config_) {
|
|
var indentUnit = editorConf.indentUnit
|
|
var config = {}
|
|
var defaults = config_.htmlMode ? htmlConfig : xmlConfig
|
|
for (var prop in defaults) config[prop] = defaults[prop]
|
|
for (var prop in config_) config[prop] = config_[prop]
|
|
|
|
// Return variables for tokenizers
|
|
var type, setStyle;
|
|
|
|
function inText(stream, state) {
|
|
function chain(parser) {
|
|
state.tokenize = parser;
|
|
return parser(stream, state);
|
|
}
|
|
|
|
var ch = stream.next();
|
|
if (ch == "<") {
|
|
if (stream.eat("!")) {
|
|
if (stream.eat("[")) {
|
|
if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
|
|
else return null;
|
|
} else if (stream.match("--")) {
|
|
return chain(inBlock("comment", "-->"));
|
|
} else if (stream.match("DOCTYPE", true, true)) {
|
|
stream.eatWhile(/[\w\._\-]/);
|
|
return chain(doctype(1));
|
|
} else {
|
|
return null;
|
|
}
|
|
} else if (stream.eat("?")) {
|
|
stream.eatWhile(/[\w\._\-]/);
|
|
state.tokenize = inBlock("meta", "?>");
|
|
return "meta";
|
|
} else {
|
|
type = stream.eat("/") ? "closeTag" : "openTag";
|
|
state.tokenize = inTag;
|
|
return "tag bracket";
|
|
}
|
|
} else if (ch == "&") {
|
|
var ok;
|
|
if (stream.eat("#")) {
|
|
if (stream.eat("x")) {
|
|
ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
|
|
} else {
|
|
ok = stream.eatWhile(/[\d]/) && stream.eat(";");
|
|
}
|
|
} else {
|
|
ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
|
|
}
|
|
return ok ? "atom" : "error";
|
|
} else {
|
|
stream.eatWhile(/[^&<]/);
|
|
return null;
|
|
}
|
|
}
|
|
inText.isInText = true;
|
|
|
|
function inTag(stream, state) {
|
|
var ch = stream.next();
|
|
if (ch == ">" || (ch == "/" && stream.eat(">"))) {
|
|
state.tokenize = inText;
|
|
type = ch == ">" ? "endTag" : "selfcloseTag";
|
|
return "tag bracket";
|
|
} else if (ch == "=") {
|
|
type = "equals";
|
|
return null;
|
|
} else if (ch == "<") {
|
|
state.tokenize = inText;
|
|
state.state = baseState;
|
|
state.tagName = state.tagStart = null;
|
|
var next = state.tokenize(stream, state);
|
|
return next ? next + " tag error" : "tag error";
|
|
} else if (/[\'\"]/.test(ch)) {
|
|
state.tokenize = inAttribute(ch);
|
|
state.stringStartCol = stream.column();
|
|
return state.tokenize(stream, state);
|
|
} else {
|
|
stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);
|
|
return "word";
|
|
}
|
|
}
|
|
|
|
function inAttribute(quote) {
|
|
var closure = function(stream, state) {
|
|
while (!stream.eol()) {
|
|
if (stream.next() == quote) {
|
|
state.tokenize = inTag;
|
|
break;
|
|
}
|
|
}
|
|
return "string";
|
|
};
|
|
closure.isInAttribute = true;
|
|
return closure;
|
|
}
|
|
|
|
function inBlock(style, terminator) {
|
|
return function(stream, state) {
|
|
while (!stream.eol()) {
|
|
if (stream.match(terminator)) {
|
|
state.tokenize = inText;
|
|
break;
|
|
}
|
|
stream.next();
|
|
}
|
|
return style;
|
|
}
|
|
}
|
|
|
|
function doctype(depth) {
|
|
return function(stream, state) {
|
|
var ch;
|
|
while ((ch = stream.next()) != null) {
|
|
if (ch == "<") {
|
|
state.tokenize = doctype(depth + 1);
|
|
return state.tokenize(stream, state);
|
|
} else if (ch == ">") {
|
|
if (depth == 1) {
|
|
state.tokenize = inText;
|
|
break;
|
|
} else {
|
|
state.tokenize = doctype(depth - 1);
|
|
return state.tokenize(stream, state);
|
|
}
|
|
}
|
|
}
|
|
return "meta";
|
|
};
|
|
}
|
|
|
|
function Context(state, tagName, startOfLine) {
|
|
this.prev = state.context;
|
|
this.tagName = tagName;
|
|
this.indent = state.indented;
|
|
this.startOfLine = startOfLine;
|
|
if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
|
|
this.noIndent = true;
|
|
}
|
|
function popContext(state) {
|
|
if (state.context) state.context = state.context.prev;
|
|
}
|
|
function maybePopContext(state, nextTagName) {
|
|
var parentTagName;
|
|
while (true) {
|
|
if (!state.context) {
|
|
return;
|
|
}
|
|
parentTagName = state.context.tagName;
|
|
if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
|
|
!config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
|
|
return;
|
|
}
|
|
popContext(state);
|
|
}
|
|
}
|
|
|
|
function baseState(type, stream, state) {
|
|
if (type == "openTag") {
|
|
state.tagStart = stream.column();
|
|
return tagNameState;
|
|
} else if (type == "closeTag") {
|
|
return closeTagNameState;
|
|
} else {
|
|
return baseState;
|
|
}
|
|
}
|
|
function tagNameState(type, stream, state) {
|
|
if (type == "word") {
|
|
state.tagName = stream.current();
|
|
setStyle = "tag";
|
|
return attrState;
|
|
} else if (config.allowMissingTagName && type == "endTag") {
|
|
setStyle = "tag bracket";
|
|
return attrState(type, stream, state);
|
|
} else {
|
|
setStyle = "error";
|
|
return tagNameState;
|
|
}
|
|
}
|
|
function closeTagNameState(type, stream, state) {
|
|
if (type == "word") {
|
|
var tagName = stream.current();
|
|
if (state.context && state.context.tagName != tagName &&
|
|
config.implicitlyClosed.hasOwnProperty(state.context.tagName))
|
|
popContext(state);
|
|
if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
|
|
setStyle = "tag";
|
|
return closeState;
|
|
} else {
|
|
setStyle = "tag error";
|
|
return closeStateErr;
|
|
}
|
|
} else if (config.allowMissingTagName && type == "endTag") {
|
|
setStyle = "tag bracket";
|
|
return closeState(type, stream, state);
|
|
} else {
|
|
setStyle = "error";
|
|
return closeStateErr;
|
|
}
|
|
}
|
|
|
|
function closeState(type, _stream, state) {
|
|
if (type != "endTag") {
|
|
setStyle = "error";
|
|
return closeState;
|
|
}
|
|
popContext(state);
|
|
return baseState;
|
|
}
|
|
function closeStateErr(type, stream, state) {
|
|
setStyle = "error";
|
|
return closeState(type, stream, state);
|
|
}
|
|
|
|
function attrState(type, _stream, state) {
|
|
if (type == "word") {
|
|
setStyle = "attribute";
|
|
return attrEqState;
|
|
} else if (type == "endTag" || type == "selfcloseTag") {
|
|
var tagName = state.tagName, tagStart = state.tagStart;
|
|
state.tagName = state.tagStart = null;
|
|
if (type == "selfcloseTag" ||
|
|
config.autoSelfClosers.hasOwnProperty(tagName)) {
|
|
maybePopContext(state, tagName);
|
|
} else {
|
|
maybePopContext(state, tagName);
|
|
state.context = new Context(state, tagName, tagStart == state.indented);
|
|
}
|
|
return baseState;
|
|
}
|
|
setStyle = "error";
|
|
return attrState;
|
|
}
|
|
function attrEqState(type, stream, state) {
|
|
if (type == "equals") return attrValueState;
|
|
if (!config.allowMissing) setStyle = "error";
|
|
return attrState(type, stream, state);
|
|
}
|
|
function attrValueState(type, stream, state) {
|
|
if (type == "string") return attrContinuedState;
|
|
if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;}
|
|
setStyle = "error";
|
|
return attrState(type, stream, state);
|
|
}
|
|
function attrContinuedState(type, stream, state) {
|
|
if (type == "string") return attrContinuedState;
|
|
return attrState(type, stream, state);
|
|
}
|
|
|
|
return {
|
|
startState: function(baseIndent) {
|
|
var state = {tokenize: inText,
|
|
state: baseState,
|
|
indented: baseIndent || 0,
|
|
tagName: null, tagStart: null,
|
|
context: null}
|
|
if (baseIndent != null) state.baseIndent = baseIndent
|
|
return state
|
|
},
|
|
|
|
token: function(stream, state) {
|
|
if (!state.tagName && stream.sol())
|
|
state.indented = stream.indentation();
|
|
|
|
if (stream.eatSpace()) return null;
|
|
type = null;
|
|
var style = state.tokenize(stream, state);
|
|
if ((style || type) && style != "comment") {
|
|
setStyle = null;
|
|
state.state = state.state(type || style, stream, state);
|
|
if (setStyle)
|
|
style = setStyle == "error" ? style + " error" : setStyle;
|
|
}
|
|
return style;
|
|
},
|
|
|
|
indent: function(state, textAfter, fullLine) {
|
|
var context = state.context;
|
|
// Indent multi-line strings (e.g. css).
|
|
if (state.tokenize.isInAttribute) {
|
|
if (state.tagStart == state.indented)
|
|
return state.stringStartCol + 1;
|
|
else
|
|
return state.indented + indentUnit;
|
|
}
|
|
if (context && context.noIndent) return CodeMirror.Pass;
|
|
if (state.tokenize != inTag && state.tokenize != inText)
|
|
return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
|
|
// Indent the starts of attribute names.
|
|
if (state.tagName) {
|
|
if (config.multilineTagIndentPastTag !== false)
|
|
return state.tagStart + state.tagName.length + 2;
|
|
else
|
|
return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);
|
|
}
|
|
if (config.alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
|
|
var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter);
|
|
if (tagAfter && tagAfter[1]) { // Closing tag spotted
|
|
while (context) {
|
|
if (context.tagName == tagAfter[2]) {
|
|
context = context.prev;
|
|
break;
|
|
} else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
|
|
context = context.prev;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} else if (tagAfter) { // Opening tag spotted
|
|
while (context) {
|
|
var grabbers = config.contextGrabbers[context.tagName];
|
|
if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
|
|
context = context.prev;
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
while (context && context.prev && !context.startOfLine)
|
|
context = context.prev;
|
|
if (context) return context.indent + indentUnit;
|
|
else return state.baseIndent || 0;
|
|
},
|
|
|
|
electricInput: /<\/[\s\w:]+>$/,
|
|
blockCommentStart: "<!--",
|
|
blockCommentEnd: "-->",
|
|
|
|
configuration: config.htmlMode ? "html" : "xml",
|
|
helperType: config.htmlMode ? "html" : "xml",
|
|
|
|
skipAttribute: function(state) {
|
|
if (state.state == attrValueState)
|
|
state.state = attrState
|
|
}
|
|
};
|
|
});
|
|
|
|
CodeMirror.defineMIME("text/xml", "xml");
|
|
CodeMirror.defineMIME("application/xml", "xml");
|
|
if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
|
|
CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});
|
|
|
|
});
|