Created
July 6, 2012 12:04
-
-
Save yuya-takeyama/3059761 to your computer and use it in GitHub Desktop.
HTML内タグの閉じ忘れをチェックする UserScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// // @name tagcheck.user.js | |
// // @namespace http://yuyat.jp/ | |
// // @description Alert if unclosed tag is detected. | |
// // @include * | |
// // ==/UserScript== | |
(function (window) { | |
"use strict"; | |
var document = window.document; | |
console.info('Checking tags for ' + document.location.href); | |
// remove inplanted script tag | |
(function () { | |
var scriptTag = document.body.lastChild; | |
if (scriptTag.tagName === 'SCRIPT') { | |
document.body.removeChild(scriptTag); | |
} | |
})(); | |
// Get html code by re-request | |
var html = (function () { | |
var ajax = (function () { | |
try { | |
return window.XMLHttpRequest ? new XMLHttpRequest() | |
: (ActiveXObject ? new ActiveXObject('Msxml2.XMLHTTP') : null); | |
} catch (e) { | |
return new ActiveXObject('Microsoft.XMLHTTP'); | |
} | |
})(); | |
ajax.open("GET", location.href, false); | |
ajax.send(''); | |
return ajax.responseText; | |
})(); | |
var opened = {}; | |
var closed = {}; | |
var errors = []; | |
// そもそも空要素のタグ | |
var EMPTYTAG = ['img', 'link', 'meta', 'br', 'hr', 'input', | |
'embed', 'area', 'base', 'basefont', 'bgsound', | |
'param', 'wbr']; | |
EMPTYTAG.indexOf = EMPTYTAG.indexOf || function (str) { | |
for (var i = 0, l = this.length; i < l; i++) { | |
if (this[i] == str) { | |
return i; | |
} | |
} | |
return -1; | |
}; | |
// 開いたまま閉じていないタグを検索する | |
(function () { | |
// 閉じタグの開始位置を返す | |
var closure = function (html, index, tagName) { | |
var closeRe = new RegExp("<(/)?" + tagName + "( [^>]*)?>", "igm"); | |
closeRe.lastIndex = index; | |
var depth = 1; | |
var r = null; | |
while (r = closeRe.exec(html)) { | |
if (r[1] == '/') { | |
if (--depth == 0) { | |
// すでに他の閉じタグになってる場合はfalse | |
return closed[r.index] ? false : { | |
head:r.index, | |
tail:r.index + r[0].length | |
}; | |
} | |
} else { | |
depth ++; | |
} | |
} | |
return false; | |
}; | |
var openPattern = /<([a-zA-Z1-9:]+)([^>]*)>/gm; | |
var found = null; | |
while(found = openPattern.exec(html)) { | |
var head = found.index; | |
var tail = head + found[0].length; | |
var tagName = found[1].toLowerCase(); | |
var attr = found[2]; | |
if (EMPTYTAG.indexOf(tagName) >= 0 || (attr && attr.charAt(attr.length - 1) == '/')) { | |
// 空要素タグ | |
closed[head] = { | |
open: head, | |
openTail: tail, | |
close: head, | |
closeTail: tail, | |
tagName: tagName, | |
attr: attr | |
}; | |
} else { | |
var cls = closure(html, tail, tagName); | |
if (cls) { | |
opened[head] = closed[cls.head] = { | |
open: head, | |
openTail: tail, | |
close: cls.head, | |
closeTail: cls.tail, | |
tagName: tagName, | |
attr: attr | |
}; | |
} else { | |
errors.push({ | |
id: errors.length, | |
head:head, | |
tail:tail, | |
tagName: tagName, | |
attr: attr, | |
message: "タグが閉じていません" | |
}); | |
} | |
} | |
openPattern.lastIndex = tail; | |
} | |
})(); | |
// 開きタグがない閉じタグを検索する | |
(function () { | |
var closePattern = /<\/([a-zA-Z1-9:]+)>/gm; | |
var found = null; | |
while(found = closePattern.exec(html)) { | |
var head = found.index; | |
var tail = head + found[0].length; | |
var tagName = found[1].toLowerCase(); | |
var attr = ''; | |
if (EMPTYTAG.indexOf(tagName) < 0) { | |
if (!closed[found.index]) { | |
errors.push({ | |
id: errors.length, | |
head:head, | |
tail:tail, | |
tagName: '/' + tagName, | |
attr: attr, | |
message: "開きタグがありません" | |
}); | |
} | |
} | |
closePattern.lastIndex = tail; | |
} | |
})(); | |
// 先に開いたタグが先に閉じているような箇所がないかチェックする | |
(function () { | |
var checked = []; | |
for (var i in opened) { | |
var cl = opened[i]; | |
for (var j = checked.length - 1; j >= 0; j--) { | |
var ch = checked[j]; | |
if (ch.open < cl.open | |
&& cl.open < ch.close | |
&& ch.close < cl.close) { | |
// 親開く-子開く-親閉じる-子閉じるの順 | |
errors.push({ | |
id: errors.length, | |
head: ch.close, | |
tail: ch.closeTail, | |
tagName: '/' + ch.tagName, | |
attr: '', | |
message: '<' + cl.tagName + cl.attr + '>よりも先に閉じてしまっています' | |
}); | |
errors.push({ | |
id: errors.length, | |
head: cl.close, | |
tail: cl.closeTail, | |
tagName: '/' + cl.tagName, | |
attr: '', | |
message: '<' + ch.tagName + ch.attr + '>よりも後で閉じてしまっています' | |
}); | |
} else if (ch.close < cl.open) { | |
// 注目している地点ですでに閉じてるのはチェックから外す | |
checked.splice(j, 1); | |
} | |
} | |
checked.push(cl); | |
} | |
})(); | |
// show sourcecode | |
(function () { | |
var sourceLine = 1; | |
// make source code html | |
var re = function (htmlCode) { | |
return htmlCode.replace(/[<>&\r\n \t]/g, function (c) { | |
switch(c) { | |
case '<': | |
return '<'; | |
case '>': | |
return '>'; | |
case '&': | |
return '&'; | |
case "\r": | |
return ''; | |
case "\n": | |
var cls = sourceLine % 2 == 0 ? 'e' : 'o'; | |
return '</div>\n<div class="ln">' + (++sourceLine) | |
+ '</div><div class="' + cls + '"> '; | |
case "\t": | |
return " "; | |
case " ": | |
return " "; | |
} | |
}); | |
} | |
var sourceCode = ['<div class="ln">1</div><div class="e"> ']; | |
errors.sort(function (a, b) { | |
return a.head - b.head; | |
}); | |
var rular = 0; | |
for (var i = 0, l = errors.length; i < l; i++) { | |
var uc = errors[i]; | |
if (rular < uc.tail) { | |
var head = re(html.substring(rular, uc.head)); | |
var tag = re(html.substring(uc.head, uc.tail)); | |
uc.lineNumber = sourceLine; | |
rular = uc.tail; | |
} | |
} | |
sourceCode.push(re(html.substring(rular)), '<br clear="all">'); | |
})(); | |
// show list | |
(function () { | |
for (var i = 0, l = errors.length; i < l; i++) { | |
var uc = errors[i]; | |
console.warn('Line ' + uc.lineNumber + ': <' + uc.tagName + '>: ' + uc.message); | |
} | |
})(); | |
}(this)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment