仿佛又回到从前codemirror1源码考古
codemirror1的源码, 每次都是简单的浏览, 从来没有认真的调试.
这次来点真格的, 我们一起读下codemirror1的源码.
从2007年读起.
- 一开始的codemirror还是相当简单的, 这大哥一开始也是喜欢写注释的, 后来就不知道注释都到哪里去了. 奇怪了, 我再看怎么都看不到任何注视了, 是我幻视了?
- 需要注意的是, 一定要加上MochiKit才能正常运行.
- 并且如果在chrome运行, 需要再文件目录起一个server才行.
- 一开始没有监听任何事件, 初始版本就2个按钮.
init版本, github最原始版本mar 2007 dda02b6
- index.html只是包含了editframe.html
- editframe.html是空的
- Util.js是工具类
- highlight.js
//highlight.js的主入口函数
function addHighlighting(id){
var textarea = $(id);
var iframe = createDOM("IFRAME", {src: "editframe.html", "class": "subtle-iframe", id: id, name: id});
textarea.parentNode.replaceChild(iframe, textarea); //这里用ifram替换了textarea
connect(iframe, "onload", stage2);//注册了载入函数
iframe.style.width = "500px";
iframe.style.height = "400px";
function stage2(){ //这个就是载入函数.
var fdoc = frames[id].document;
fdoc.designMode = "on"; //开启了designmode.
importCode(textarea.value, fdoc.body); //这个就是innerhtml的一次替换. 主要是处理回车和空格. 变成br和nbsp.
highlight(fdoc.body);//主处理函数
}
}
function highlight(node){
if (!node.firstChild)
return;
var dom = traverseDOM(node.firstChild);
var parsed = parse(iconcat(dom));
var split = splitBy(function(t){return t.type == "newline";}, parsed);
var line = null;
function partLength(part){
return part.firstChild.textContent.length;
}
function correctPart(token, part){
return part.firstChild.textContent == token.value && hasElementClass(part, token.style);
}
function shortenPart(part, minus){
part.firstChild.textContent = part.firstChild.textContent.substring(minus);
}
function removePart(part){
var nextpart = part.nextSibling;
line.removeChild(part);
return nextpart;
}
function tokenPart(token){
return SPAN({"class": "part " + token.style}, token.value);
}
forEach(split, function(tokens){
line = line ? line.nextSibling : node.firstChild;
console.log("Line = " + scrapeText(line) + ", tokens = " + map(itemgetter("value"), tokens));
var part = null;
forEach(tokens, function(token){
console.log("Token '" + token.value + "'");
if (!part) part = line.firstChild;
var tokensize = token.value.length;
if (correctPart(token, part)){
part = part.nextSibling;
}
else {
line.insertBefore(tokenPart(token), part);
while (tokensize > 0) {
var partsize = partLength(part);
if (partsize > tokensize){
shortenPart(part, tokensize);
tokensize = 0;
}
else {
tokensize -= partsize;
part = removePart(part);
}
}
}
});
});
}
第一个版本的代码大概就看到这里, 我们可以看到code1的初始目标还是比较简单的: 做一个highlight. 并且, 也没有考虑格式分离这类事. 并且我们能看到作者大量使用正则.
后续几个小版本
合在一起说, 等出现大变化, 我们再单独分析.
- 去掉div, 用span和br处理整个文档.
- 改小bug.
- 和IE搏斗. 例如
- 为了让ie正常, 不在引入iframe的html, 而是直接硬编码, 写到代码里面. 用了element.write(“<div>这里是编码</div>”)
- ie的空白字符, 比如回车是\r\n
我抽取了一个版本mar2007, bff2e25, 感觉designmode确实更好用. 我要实验一下.
重要结论: designmode的tab处理本身就是对的. contenteditable的tab就必须要处理了.
那么我的疑问出现了, code1的最后一个版本是怎样的呢? designmode还是contenteditable?
尝试阅读code1的最后一个版本.
js处理的文件是: jstest.html, 很神奇, 这个版本是可以直接运行的. 不需要serve, 也不需要mochikit.
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="js/codemirror.js" type="text/javascript"></script>
<script src="js/mirrorframe.js" type="text/javascript"></script>
<title>CodeMirror: JavaScript demonstration</title>
<link rel="stylesheet" type="text/css" href="css/docs.css"/>
</head>
<body style="padding: 20px;">
<div>
<textarea id="code" cols="120" rows="30">
// Here you see some JavaScript code. Mess around with it to get
// acquainted with CodeMirror's features.
// Press enter inside the object and your new line will be suitably
// indented.
var keyBindings = {
enter: "newline-and-indent",
tab: "reindent-selection",
ctrl_z: "undo",
ctrl_y: "redo",
ctrl_bracket: "highlight-brackets",
ctrl_shift_bracket: "jump-to-matching-bracket"
};
// Press tab on the next line and the wrong indentation will be fixed.
var regex = /foo|bar/i;
function example(x) {
// Local variables get a different colour than global ones.
var y = 44.4;
return x + y - z;
}
</textarea>
</div>
<script type="text/javascript">
var textarea = document.getElementById('code'); //拿到textarea.
var editor = new MirrorFrame(CodeMirror.replace(textarea), {
//看上去这里制造了一个frame
height: "350px",
content: textarea.value,
parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
stylesheet: "css/jscolors.css",
path: "js/",
autoMatchParens: true
});
</script>
</body>
</html>
一共两个js, 我们先看看mirrorframe.js
//入口主函数看上去就是这个.
function MirrorFrame(place, options) { //第二个参数只是给codemirror使用.
this.home = document.createElement("div");
if (place.appendChild) //place是第一个参数, 是codemirror给过来的. 现在去看看codemirror.
place.appendChild(this.home);
else
place(this.home);
var self = this;
function makeButton(name, action) {
var button = document.createElement("input");
button.type = "button";
button.value = name;
self.home.appendChild(button);
button.onclick = function(){self[action].call(self);};
}
makeButton("Search", "search");
makeButton("Replace", "replace");
makeButton("Current line", "line");
makeButton("Jump to line", "jump");
makeButton("Insert constructor", "macro");
makeButton("Indent all", "reindent");
this.mirror = new CodeMirror(this.home, options);//这里可以看到参数都给codemirror使用了. this.home就是这里第一行制造的div.
}
再看看codemirror.js, 第一步先看看上面直接用到的入口: CodeMirror.replace
CodeMirror.replace = function(element) {
if (typeof element == "string")
element = document.getElementById(element);
return function(newElement) {
element.parentNode.replaceChild(newElement, element);
};
}; //这里显然没写太多东西, 拿的东西再哪里? 恐怕就在于直接执行的代码. 但是还是要承认作者使用了高阶函数, 这个啥意思? 要查一下. 幸亏我有作者的书.
因为前面的没看明白, 所以看看codemirror是啥吧.
//codemirror整体是一个闭包, 返回一个codemirror. 从121-178行定义了这个对象的主体.
function CodeMirror(place, options) {
// Use passed options, if any, to override defaults.
this.options = options = options || {};
setDefaults(options, CodeMirrorConfig);
// Backward compatibility for deprecated options.
if (options.dumbTabs) options.tabMode = "spaces";
else if (options.normalTab) options.tabMode = "default";
if (options.cursorActivity) options.onCursorActivity = options.cursorActivity;
var frame = this.frame = createHTMLElement("iframe");
if (options.iframeClass) frame.className = options.iframeClass;
frame.frameBorder = 0;
frame.style.border = "0";
frame.style.width = '100%';
frame.style.height = '100%';
// display: block occasionally suppresses some Firefox bugs, so we
// always add it, redundant as it sounds.
frame.style.display = "block";
var div = this.wrapping = createHTMLElement("div");
div.style.position = "relative";
div.className = "CodeMirror-wrapping";
div.style.width = options.width;
div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
// This is used by Editor.reroutePasteEvent
var teHack = this.textareaHack = createHTMLElement("textarea");
div.appendChild(teHack);
teHack.style.position = "absolute";
teHack.style.left = "-10000px";
teHack.style.width = "10px";
teHack.tabIndex = 100000;
// Link back to this object, so that the editor can fetch options
// and add a reference to itself.
frame.CodeMirror = this;
if (options.domain && internetExplorer) {
this.html = frameHTML(options);
frame.src = "javascript:(function(){document.open();" +
(options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
"document.write(window.frameElement.CodeMirror.html);document.close();})()";
}
else {
frame.src = "javascript:;";
}
if (place.appendChild) place.appendChild(div);
else place(div);
div.appendChild(frame);
if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div, options.firstLineNumber);
this.win = frame.contentWindow;
if (!options.domain || !internetExplorer) {
this.win.document.open();
this.win.document.write(frameHTML(options));
this.win.document.close();
}
}
- 然后我发现了代码, 作者把这部分代码分散在了editor.js里面, 作者确实是优先使用contenteditable的.
- 作者响应了下面三个事件
- keydown
- keypress
- keup
- 但是, 看到这里我还是没有解决codemirror.replace是高阶函数, 他的参数是如何传入的问题.
通过跟踪代码, 我知道了.
//在jstest.html里面:
var editor = new MirrorFrame(CodeMirror.replace(textarea), {;//...
})
//这里第一个参数CodeMirror.replace(textarea)确实是一个函数
//然后在MirrorFrame里面, 这个函数得到了调用
function MirrorFrame(place, options) { //第二个参数只是给codemirror使用.
this.home = document.createElement("div");
if (place.appendChild) //place是第一个参数, 是codemirror给过来的. 现在去看看codemirror.
place.appendChild(this.home);
else
place(this.home);//就是这里, 第一个参数作为函数被调用了.
//...
}
继续跟踪代码, 新的问题出现了, 系统逐个加载js, 可是, 这些js是注册再哪里的呢?
- jstest.html里面注册了一个div. 这个div就是mirrorframe里面的this.home, 然后这个div被传给了codemirror.replace, 然后这些和frame其实都没有关系.
- mirrorframe就是把本来页面上的textarea替换成了他自己的那一堆button.
- mirrorframe初始化的最后一句: this.mirror = new CodeMirror(this.home, options); 这里和codemirror发生了关系.
- codemirror闭包返回的其实是他自己的构造函数.
- 有关系的是codemirror的初始化函数. 自动执行的闭包里面定义了自己的frame.
- 然后用place(第一个参数)放置了codemirror制造的div, textarea, iframe. 而这个参数再mirrorframe调用的时候, 传入的是mirrorframe制造的那个包含很多button的div.
然后我们重新阅读codemirror构造函数:
function CodeMirror(place, options) {
// Use passed options, if any, to override defaults.
this.options = options = options || {};
setDefaults(options, CodeMirrorConfig);
// Backward compatibility for deprecated options.
if (options.dumbTabs) options.tabMode = "spaces";
else if (options.normalTab) options.tabMode = "default";
if (options.cursorActivity) options.onCursorActivity = options.cursorActivity; //这个是啥, 值得研究一下, todo
var frame = this.frame = createHTMLElement("iframe");
//制造iframe
if (options.iframeClass) frame.className = options.iframeClass;
frame.frameBorder = 0;
frame.style.border = "0";
frame.style.width = '100%';
frame.style.height = '100%';
// display: block occasionally suppresses some Firefox bugs, so we
// always add it, redundant as it sounds.
frame.style.display = "block";
var div = this.wrapping = createHTMLElement("div");
//看字面意思, 这事要做一个自动换行的div. 而且貌似是在iframe外边的.
div.style.position = "relative";
div.className = "CodeMirror-wrapping";
div.style.width = options.width;
div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
// This is used by Editor.reroutePasteEvent
var teHack = this.textareaHack = createHTMLElement("textarea");
//这里有意思了, 又新建了一个textarea, 嘿嘿, 我把他显示出来看看.
div.appendChild(teHack);
teHack.style.position = "absolute";
teHack.style.left = "100px";//原文这里是看不到的, 我改了一下.
teHack.style.width = "100px";
teHack.tabIndex = 100000;
// Link back to this object, so that the editor can fetch options
// and add a reference to itself.
frame.CodeMirror = this;
if (options.domain && internetExplorer) {
this.html = frameHTML(options); //如果ie也会调这个代码.
frame.src = "javascript:(function(){document.open();" +
(options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
"document.write(window.frameElement.CodeMirror.html);document.close();})()";
} else {
frame.src = "javascript:;";
}
if (place.appendChild) place.appendChild(div);
else place(div); //codemirror用第一个参数, 放置这个div.
div.appendChild(frame); //果然div里面放iframe.
if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div, options.firstLineNumber);
this.win = frame.contentWindow;
if (!options.domain || !internetExplorer) {
this.win.document.open();
this.win.document.write(frameHTML(options));//不是ie也调这个.
//貌似这个地方是关键, 继续研究一下
this.win.document.close();
}
}
研究内容:
- onCursorActivity , 这个单独看貌似没啥意思, 等后面再碰到再说.
- framehtml, 这个就是核心了, 我们一起来看一下.
排好队, 一个一个来.
function frameHTML(options) {
if (typeof options.parserfile == "string")
options.parserfile = [options.parserfile];
if (typeof options.basefiles == "string")
options.basefiles = [options.basefiles];
if (typeof options.stylesheet == "string")
options.stylesheet = [options.stylesheet];
var sp = " spellcheck=\"" + (options.disableSpellcheck ? "false" : "true") + "\"";
var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html" + sp + "><head>"];
// Hack to work around a bunch of IE8-specific problems.
html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
var queryStr = options.noScriptCaching ? "?nocache=" + new Date().getTime().toString(16) : "";
forEach(options.stylesheet, function (file) {
html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + queryStr + "\"/>");
});
forEach(options.basefiles.concat(options.parserfile), function (file) {
if (!/^https?:/.test(file)) file = options.path + file;
html.push("<script type=\"text/javascript\" src=\"" + file + queryStr + "\"><" + "/script>");
});
html.push("</head><body style=\"border-width: 0;\" class=\"editbox\"" + sp + "></body></html>");
return html.join("");
}
然后, 我看到了这个:
setDefaults(CodeMirrorConfig, {
stylesheet: [],
path: "",
parserfile: [],
basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
iframeClass: null,
passDelay: 200,
passTime: 50,
})
//初始的jstest.html里面也写了一点:
parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
stylesheet: "css/jscolors.css",
然后, 这个iframe需要的js就都被引入了.
- “util.js”, 作者说这里是工具.
- “stringstream.js”, 这里是要用来parse的内容.
- “select.js”, 这里处理选中, 针对w3c和ie各写了一套.
- “undo.js”, 这里处理undo
- “editor.js”, 这个处理editor未来要细看.
- “tokenize.js” 这个处理换行和空格这样的分隔符的, 需要细看.
- “tokenizejavascript.js”, 单独给javascript用的.
- “parsejavascript.js” js的parser, 如果就是处理括号和引号, 那么其实不需要这个.
editor.js
这个看起来就很熟悉的感觉了.
-
处理空格折叠问题, 第二个空格开始nbsp
-
……一路看下去.
-
keydown: 这里处理tab和回车.
-
keypress: 这里看是否要更新缩进.
-
keyup: 这里处理本行是否dirty(需要重新高亮). 处理6个光标按键: shift, control, alt, arrows, home,end
那么, 问题来了: delete和backspace什么时候处理?
- 都在keypress处理的. code分别为8和46.
- 还处理了code13 回车
- code9, 这个我记得是tab
- code32 空格
keydown处理了很多key
- 13
- 9
- 32
- 36 home
- 35 end
- 33 page up
- 34 page down
- 219和221 中括号
- 37 39 + meta (左右)
- 回退, 保存, 粘贴, 都分别处理了.
另外, 这里看到了: cursorActivity, 他会把某一行设置为dirty, 并且确保highlight执行.
成功没有侥幸, 这些代码令人肃然起敬, 作者就是用笨办法, 一个一个都处理了. 不禁让我想起tsp专家再[迷茫的旅行商]里面说的话, 2交换很简单, 5交换就要处理128种情况, 我们发现大神并没有回避, 而是真的逐一处理了这些情况, 最终拿出了一个最牛的方案.
至此code1主体阅读完毕. 未来需要细节我会再写续篇.