Permalink
Browse files

Add new state introspection mechanism for nested modes

And move closetag over to it.

This makes the code that gets the XML state out of the mode actually
sound and extensible.

Issue #820
  • Loading branch information...
marijnh committed Sep 12, 2012
1 parent acb6aa6 commit 23c8c3e79ab2ae3e4127150482f94a850a7a8516
View
@@ -531,6 +531,11 @@ <h2 id="api">Programming API</h2>
<dt id="getOption"><code>getOption(option) → value</code></dt>
<dd>Retrieves the current value of the given option for this
editor instance.</dd>
+ <dt id="getMode"><code>getMode() → object</code></dt>
+ <dd>Gets the mode object for the editor. Note that this is
+ distinct from <code>getOption("mode")</code>, which gives you
+ the mode specification, rather than the resolved, instantiated
+ <a href="#defineMode">mode object</a>.</dd>
<dt id="cursorCoords"><code>cursorCoords(start, mode) → object</code></dt>
<dd>Returns an <code>{x, y, yBot}</code> object containing the
@@ -1154,6 +1159,14 @@ <h2 id="modeapi">Writing CodeMirror Modes</h2>
where <code>mode</code> is the mode that created the given
state.</p>
+ <p id="innerMode">In a nested mode, it is recommended to add an
+ extra methods, <code>innerMode</code> which, given a state object,
+ returns a <code>{state, mode}</code> object with the inner mode
+ and its state for the current position. These are used by utility
+ scripts such as the autoformatter and
+ the <a href="#util_closetag">tag closer</a> to get context
+ information.</p>
+
<p>To make indentation work properly in a nested parser, it is
advisable to give the <code>startState</code> method of modes that
are intended to be nested an optional argument that provides the
@@ -1172,6 +1185,15 @@ <h2 id="modeapi">Writing CodeMirror Modes</h2>
mode, as in the <a href="#option_mode"><code>mode</code></a>
option.</p>
+ <p id="extendMode">Sometimes, it is useful to add or override mode
+ object properties from external code.
+ The <code>CodeMirror.extendMode</code> can be used to add
+ properties to mode objects produced for a specific mode. Its first
+ argument is the name of the mode, its second an object that
+ specifies the properties that should be added. This is mostly
+ useful to add utilities that can later be looked
+ up <a href="#getMode"><code>getMode</code></a>.</p>
+
</div><div class="rightsmall blk">
<h2>Contents</h2>
View
@@ -174,6 +174,7 @@ window.CodeMirror = (function() {
}
},
getOption: function(option) {return options[option];},
+ getMode: function() {return mode;},
undo: operation(undo),
redo: operation(redo),
indentLine: operation(function(n, dir) {
@@ -2037,7 +2038,13 @@ window.CodeMirror = (function() {
var spec = CodeMirror.resolveMode(spec);
var mfactory = modes[spec.name];
if (!mfactory) return CodeMirror.getMode(options, "text/plain");
- return mfactory(options, spec);
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop];
+ }
+ modeObj.name = spec.name;
+ return modeObj;
};
CodeMirror.listModes = function() {
var list = [];
@@ -2057,6 +2064,13 @@ window.CodeMirror = (function() {
extensions[name] = func;
};
+ var modeExtensions = CodeMirror.modeExtensions = {};
+ CodeMirror.extendMode = function(mode, properties) {
+ var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
+ for (var prop in properties) if (properties.hasOwnProperty(prop))
+ exts[prop] = properties[prop];
+ };
+
var commands = CodeMirror.commands = {
selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});},
killLine: function(cm) {
View
@@ -26,16 +26,15 @@
/** Array of tag names where an end tag is forbidden. */
CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
- function resolveMode(mode) {
- if (typeof mode == "object") return mode.name;
- if (mode.indexOf("/") > -1) return resolveMode(CodeMirror.mimeModes[mode]);
- else return mode;
+ function innerState(cm, state) {
+ for (var mode = cm.getMode(); mode.innerMode;) {
+ var info = mode.innerMode(state);
+ mode = info.mode;
+ state = info.state;
+ }
+ if (mode.name == "xml") return state;
}
- function innerState(state) {
- // htmlmixed uses .htmlState, PHP .html, XML just the top state object
- return state.htmlState || state.html || state;
- }
/**
* Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
@@ -50,39 +49,34 @@
throw CodeMirror.Pass;
}
+ /*
+ * Relevant structure of token:
+ *
+ * htmlmixed
+ * className
+ * state
+ * htmlState
+ * type
+ * tagName
+ * context
+ * tagName
+ * mode
+ *
+ * xml
+ * className
+ * state
+ * tagName
+ * type
+ */
- if (/^(xml|php|htmlmixed)$/.test(resolveMode(cm.getOption('mode')))) {
-
- /*
- * Relevant structure of token:
- *
- * htmlmixed
- * className
- * state
- * htmlState
- * type
- * tagName
- * context
- * tagName
- * mode
- *
- * xml
- * className
- * state
- * tagName
- * type
- */
-
- var pos = cm.getCursor();
- var tok = cm.getTokenAt(pos);
- var state = tok.state;
-
- if (state.mode && state.mode != 'html') {
- throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode.
- }
+ var pos = cm.getCursor();
+ var tok = cm.getTokenAt(pos);
+ var state = innerState(cm, tok.state);
+
+ if (state) {
if (ch == '>') {
- var type = innerState(state).type;
+ var type = state.type;
if (tok.className == 'tag' && type == 'closeTag') {
throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
@@ -93,11 +87,12 @@
cm.setCursor(pos);
tok = cm.getTokenAt(cm.getCursor());
- state = tok.state;
- var type = innerState(state).type;
+ state = innerState(cm, tok.state);
+ if (!state) throw CodeMirror.Pass;
+ var type = state.type;
if (tok.className == 'tag' && type != 'selfcloseTag') {
- var tagName = innerState(state).tagName;
+ var tagName = state.tagName;
if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
insertEndTag(cm, indent, pos, tagName);
}
@@ -110,7 +105,7 @@
} else if (ch == '/') {
if (tok.className == 'tag' && tok.string == '<') {
- var ctx = innerState(state).context, tagName = ctx ? ctx.tagName : '';
+ var ctx = state.context, tagName = ctx ? ctx.tagName : '';
if (tagName.length > 0) {
completeEndTag(cm, pos, tagName);
return;
View
@@ -68,6 +68,10 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
return mode.indent(state.innerActive ? state.inner : state.outer, textAfter);
},
- electricChars: outer.electricChars
+ electricChars: outer.electricChars,
+
+ innerMode: function(state) {
+ return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer};
+ }
};
};
View
@@ -47,6 +47,8 @@ CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, comb
indent: base.indent && function(state, textAfter) {
return base.indent(state.base, textAfter);
},
- electricChars: base.electricChars
+ electricChars: base.electricChars,
+
+ innerMode: function(state) { return {state: state.base, mode: base}; }
};
};
@@ -58,8 +58,12 @@ 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");
@@ -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;
@@ -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,7 +72,12 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) {
return cssMode.indent(state.localState, textAfter);
},
- 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");
View
@@ -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");

0 comments on commit 23c8c3e

Please sign in to comment.