438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
class BaseHighlighter{
|
||
|
constructor(){
|
||
|
this.state = BaseHighlighter.createState();
|
||
|
}
|
||
|
reset(){
|
||
|
this.state.reset();
|
||
|
}
|
||
|
parse(){
|
||
|
if(!this.state.initialState){
|
||
|
throw "This highlighter is not initialized";
|
||
|
}
|
||
|
}
|
||
|
static createState(){
|
||
|
return new (function(){
|
||
|
this.token = "";
|
||
|
let current = null;
|
||
|
let previous = null;
|
||
|
let initialState = null;
|
||
|
this.set = (a) => {
|
||
|
previous = current;
|
||
|
current = a;
|
||
|
return
|
||
|
}
|
||
|
this.now = () => current;
|
||
|
this.previous = ()=> previous;
|
||
|
this.initializeTo = (some) => { initialState = some }
|
||
|
this.reset = () => {
|
||
|
this.token = "";
|
||
|
previous = initialState;
|
||
|
current = initialState;
|
||
|
}
|
||
|
})();
|
||
|
}
|
||
|
static BaseState = Symbol("base");
|
||
|
}
|
||
|
|
||
|
class SimpleHighlighter extends BaseHighlighter{
|
||
|
constructor(){
|
||
|
super();
|
||
|
this.state.initializeTo(BaseHighlighter.BaseState);
|
||
|
}
|
||
|
static State = {
|
||
|
SingleQuote : Symbol("quote"),
|
||
|
DoubleQuote : Symbol("quote")
|
||
|
}
|
||
|
|
||
|
parse(info,targetNode){
|
||
|
if(info.content.length > 0){
|
||
|
SimpleHighlighter.parseSimple(info,this.state,targetNode);
|
||
|
}
|
||
|
if(this.state.token){
|
||
|
throw "simple token is not 0"
|
||
|
}
|
||
|
this.state.token = "";
|
||
|
}
|
||
|
|
||
|
static parseSimple(info,state,target){
|
||
|
let pointer = 0;
|
||
|
let currentState = state.now();
|
||
|
const length = info.content.length;
|
||
|
while(pointer < length){
|
||
|
let character = info.content[pointer++];
|
||
|
if(character === "\r"){
|
||
|
continue
|
||
|
}
|
||
|
currentState = state.now();
|
||
|
switch(currentState){
|
||
|
case BaseHighlighter.BaseState:
|
||
|
switch(character){
|
||
|
case "\"":
|
||
|
target.append(state.token);
|
||
|
state.token = "\"";
|
||
|
state.set(SimpleHighlighter.State.DoubleQuote);
|
||
|
break;
|
||
|
case "'":
|
||
|
targetNode.append(state.token);
|
||
|
state.token = "'";
|
||
|
state.set(SimpleHighlighter.State.SingleQuote);
|
||
|
break;
|
||
|
default:
|
||
|
state.token += character;
|
||
|
}
|
||
|
break;
|
||
|
case SimpleHighlighter.State.SingleQuote:
|
||
|
switch(character){
|
||
|
case "'":
|
||
|
target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token + "'";
|
||
|
state.token = "";
|
||
|
state.set(BaseHighlighter.BaseState);
|
||
|
break;
|
||
|
default:
|
||
|
state.token += character;
|
||
|
}
|
||
|
break;
|
||
|
case SimpleHighlighter.State.DoubleQuote:
|
||
|
switch(character){
|
||
|
case "\"":
|
||
|
target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token + "\"";
|
||
|
state.token = "";
|
||
|
state.set(BaseHighlighter.BaseState);
|
||
|
break;
|
||
|
default:
|
||
|
state.token += character;
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if(state.token.length > 0){
|
||
|
if(currentState === BaseHighlighter.BaseState){
|
||
|
target.append(state.token);
|
||
|
state.token = "";
|
||
|
}else{
|
||
|
target.appendChild(SimpleHighlighter.createQuote()).textContent = state.token;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static createQuote(){
|
||
|
let n = document.createElement("span");
|
||
|
n.className = "quote";
|
||
|
return n
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class CSSHighlighter extends BaseHighlighter{
|
||
|
constructor(){
|
||
|
super();
|
||
|
this.state.initializeTo(CSSHighlighter.State.Selector);
|
||
|
this.state.curly = false;
|
||
|
this.state.fnLevel = 0;
|
||
|
this.state.generateLinks = false;
|
||
|
}
|
||
|
|
||
|
reset(){
|
||
|
this.state.reset();
|
||
|
this.state.curly = false;
|
||
|
this.state.fnLevel = 0;
|
||
|
}
|
||
|
|
||
|
parse(info,targetNode){
|
||
|
if(info.content.length > 0){
|
||
|
CSSHighlighter.parseCSS(info,this.state,targetNode);
|
||
|
}
|
||
|
if(this.state.token){
|
||
|
throw "CSS token is not 0"
|
||
|
}
|
||
|
this.state.token = "";
|
||
|
}
|
||
|
|
||
|
static parseCSS(info,state,targetNode){
|
||
|
|
||
|
state.generateLinks = (info.linkMatcher instanceof RegExp) && (typeof info.linkGenerator === "function");
|
||
|
|
||
|
if(state.generateLinks && info.linkChanged){
|
||
|
if (info.linkGenerator != state.linkGenerator){
|
||
|
state.linkGenerator = info.linkGenerator;
|
||
|
}
|
||
|
if (info.linkMatcher != state.linkMatcher){
|
||
|
state.linkMatcher = info.linkMatcher;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let currentState;
|
||
|
const chars = Array.from(info.content);
|
||
|
if(info.content.endsWith("\r\n")){
|
||
|
chars[chars.length - 2] = "\n";
|
||
|
chars.pop();
|
||
|
}
|
||
|
const length = chars.length;
|
||
|
let tokenStart = 0;
|
||
|
let pointer = 0;
|
||
|
while(pointer < length){
|
||
|
let character = chars[pointer];
|
||
|
currentState = state.now();
|
||
|
switch(currentState){
|
||
|
|
||
|
case CSSHighlighter.State.Selector:
|
||
|
switch(character){
|
||
|
case "/":
|
||
|
if(chars[pointer+1] === "*"){
|
||
|
state.set(CSSHighlighter.State.Comment);
|
||
|
if(pointer - tokenStart > 0){
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Selector,targetNode);
|
||
|
}
|
||
|
tokenStart = pointer;
|
||
|
pointer++;
|
||
|
}
|
||
|
break;
|
||
|
case "{":
|
||
|
state.set(CSSHighlighter.State.Property);
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Selector,targetNode);
|
||
|
tokenStart = pointer + 1;
|
||
|
targetNode.appendChild(CSSHighlighter.addBracket("{"));
|
||
|
break;
|
||
|
case "}":
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Text,targetNode);
|
||
|
tokenStart = pointer + 1;
|
||
|
|
||
|
targetNode.appendChild(CSSHighlighter.addBracket("}"));
|
||
|
|
||
|
break;
|
||
|
case "@":
|
||
|
state.set(CSSHighlighter.State.AtRule);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case CSSHighlighter.State.Comment:
|
||
|
if(character === "*"){
|
||
|
if(chars[pointer+1] === "/"){
|
||
|
pointer++;
|
||
|
state.token = chars.slice(tokenStart,pointer+1).join("");
|
||
|
state.set(state.previous());
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Comment,targetNode);
|
||
|
|
||
|
tokenStart = pointer + 1;
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case CSSHighlighter.State.Property:
|
||
|
switch(character){
|
||
|
case "/":
|
||
|
if(chars[pointer+1] === "*"){
|
||
|
state.set(CSSHighlighter.State.Comment);
|
||
|
}
|
||
|
break;
|
||
|
case ":":
|
||
|
state.token = chars.slice(tokenStart,pointer+1).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Property,targetNode);
|
||
|
state.set(CSSHighlighter.State.Value);
|
||
|
tokenStart = pointer + 1;
|
||
|
break;
|
||
|
case "}":
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Text,targetNode);
|
||
|
state.set(CSSHighlighter.State.Selector);
|
||
|
tokenStart = pointer + 1;
|
||
|
targetNode.appendChild(CSSHighlighter.addBracket("}"));
|
||
|
}
|
||
|
break;
|
||
|
case CSSHighlighter.State.Value:
|
||
|
let createToken = true;
|
||
|
let indexOffset = 1;
|
||
|
switch(character){
|
||
|
case ";":
|
||
|
state.set(CSSHighlighter.State.Property);
|
||
|
break;
|
||
|
case "}":
|
||
|
indexOffset = 0;
|
||
|
state.set(CSSHighlighter.State.Selector);
|
||
|
break;
|
||
|
case "(":
|
||
|
state.fnLevel++;
|
||
|
state.set(CSSHighlighter.State.Function);
|
||
|
break;
|
||
|
default:
|
||
|
createToken = false;
|
||
|
}
|
||
|
if(createToken){
|
||
|
state.token = chars.slice(tokenStart,pointer+indexOffset).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Value,targetNode);
|
||
|
tokenStart = pointer + 1;
|
||
|
if(indexOffset === 0){
|
||
|
targetNode.appendChild(CSSHighlighter.addBracket("}"))
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case CSSHighlighter.State.AtRule:
|
||
|
switch(character){
|
||
|
case " ":
|
||
|
state.token = chars.slice(tokenStart,pointer+1).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.AtRule,targetNode);
|
||
|
state.set(CSSHighlighter.State.AtValue);
|
||
|
tokenStart = pointer + 1;
|
||
|
}
|
||
|
break;
|
||
|
case CSSHighlighter.State.AtValue:
|
||
|
let idxOffset = 0;
|
||
|
switch(character){
|
||
|
case ";":
|
||
|
indexOffset = 1;
|
||
|
case "{":
|
||
|
state.token = chars.slice(tokenStart,pointer + idxOffset).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.AtValue,targetNode);
|
||
|
state.set(CSSHighlighter.State.Selector);
|
||
|
tokenStart = pointer + 1;
|
||
|
if(!idxOffset){
|
||
|
targetNode.appendChild(CSSHighlighter.addBracket("{"));
|
||
|
}
|
||
|
default:
|
||
|
}
|
||
|
break
|
||
|
case CSSHighlighter.State.Function:
|
||
|
switch(character){
|
||
|
case ")":
|
||
|
state.fnLevel--;
|
||
|
if(state.fnLevel === 0){
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,CSSHighlighter.State.Function,targetNode);
|
||
|
tokenStart = pointer;
|
||
|
state.set(CSSHighlighter.State.Value);
|
||
|
}
|
||
|
break;
|
||
|
case "}":
|
||
|
state.fnLevel = 0;
|
||
|
state.set(CSSHighlighter.State.Selector);
|
||
|
}
|
||
|
}
|
||
|
pointer++
|
||
|
}
|
||
|
if(pointer > tokenStart){
|
||
|
state.token = chars.slice(tokenStart,pointer).join("");
|
||
|
CSSHighlighter.createElementFromToken(state,currentState,targetNode);
|
||
|
}
|
||
|
state.token = "";
|
||
|
}
|
||
|
|
||
|
static State = {
|
||
|
Selector: Symbol("selector"),
|
||
|
Text: Symbol("text"),
|
||
|
Comment: Symbol("comment"),
|
||
|
Property: Symbol("property"),
|
||
|
Value: Symbol("value"),
|
||
|
AtRule: Symbol("atrule"),
|
||
|
AtValue: Symbol("atvalue"),
|
||
|
Function: Symbol("function"),
|
||
|
Curly: Symbol("curly")
|
||
|
}
|
||
|
|
||
|
static selectorToClassMap = new Map([
|
||
|
[":","pseudo"],
|
||
|
["#","id"],
|
||
|
[".","class"],
|
||
|
["[","attribute"]
|
||
|
]);
|
||
|
|
||
|
static addBracket(n){
|
||
|
let span = document.createElement("span");
|
||
|
span.className = "curly";
|
||
|
span.textContent = n;
|
||
|
return span
|
||
|
}
|
||
|
|
||
|
static createElementFromToken(state,type,targetNode){
|
||
|
if(state.token.length === 0){
|
||
|
return
|
||
|
}
|
||
|
let n = document.createElement("span");
|
||
|
switch(type){
|
||
|
case CSSHighlighter.State.Selector:
|
||
|
// This isn't exactly correct, but it works because parser treats \r\n sequences that follow a closed comment as "selector"
|
||
|
//rulesetUnderConstruction = createNewRuleset();
|
||
|
let parts = state.token.split(/([\.#:\[]\w[\w-_"'=\]]*|\s\w[\w-_"'=\]]*)/);
|
||
|
|
||
|
for(let part of parts){
|
||
|
if(part.length === 0){
|
||
|
continue
|
||
|
}
|
||
|
let character = part[0];
|
||
|
switch (character){
|
||
|
case ":":
|
||
|
case "#":
|
||
|
case "[":
|
||
|
case ".":
|
||
|
let p = n.appendChild(document.createElement("span"));
|
||
|
p.className = CSSHighlighter.selectorToClassMap.get(character);
|
||
|
p.textContent = part;
|
||
|
break;
|
||
|
default:
|
||
|
n.append(part);
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
case CSSHighlighter.State.Comment:
|
||
|
if(state.generateLinks){
|
||
|
let linksToFile = state.token.match(state.linkMatcher);
|
||
|
if(linksToFile && linksToFile.length){
|
||
|
const transformed = linksToFile.map(state.linkGenerator);
|
||
|
n.append(CSSHighlighter.createLinksFromMatchingToken(linksToFile,transformed,state));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
n.textContent = state.token;
|
||
|
break;
|
||
|
case CSSHighlighter.State.Value:
|
||
|
let startImportant = state.token.indexOf("!important");
|
||
|
if(startImportant === -1){
|
||
|
n.textContent = state.token;
|
||
|
}else{
|
||
|
n.textContent = state.token.substr(0,startImportant);
|
||
|
let importantTag = document.createElement("span");
|
||
|
importantTag.className = "important-tag";
|
||
|
importantTag.textContent = "!important";
|
||
|
n.appendChild(importantTag);
|
||
|
if(state.token.length > (9 + startImportant)){
|
||
|
n.append(state.token.substr(startImportant + 10))
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case CSSHighlighter.State.Function:
|
||
|
n.textContent = state.token;
|
||
|
break
|
||
|
default:
|
||
|
n.textContent = state.token;
|
||
|
}
|
||
|
|
||
|
n.className = (`token ${type.description}`);
|
||
|
|
||
|
targetNode.appendChild(n);
|
||
|
return
|
||
|
}
|
||
|
static createLinksFromMatchingToken(parts,transformed,state){
|
||
|
let frag = new DocumentFragment();
|
||
|
let linkIdx = 0;
|
||
|
let fromIdx = 0;
|
||
|
while(linkIdx < parts.length){
|
||
|
let part = parts[linkIdx];
|
||
|
let idx = state.token.indexOf(part);
|
||
|
frag.append(state.token.substring(fromIdx,idx));
|
||
|
let link = document.createElement("a");
|
||
|
link.textContent = part;
|
||
|
link.href = transformed[linkIdx++];
|
||
|
link.target = "_blank";
|
||
|
frag.appendChild(link);
|
||
|
fromIdx = idx + part.length;
|
||
|
}
|
||
|
frag.append(state.token.substring(fromIdx));
|
||
|
return frag
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { CSSHighlighter,SimpleHighlighter }
|