Merge branch 'codeblock'
This commit is contained in:
		
						commit
						cdeb11acfc
					
				
					 6 changed files with 991 additions and 286 deletions
				
			
		
							
								
								
									
										438
									
								
								html_resources/code-block/code-block-highlighter.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								html_resources/code-block/code-block-highlighter.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,438 @@
 | 
			
		|||
'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 }
 | 
			
		||||
							
								
								
									
										122
									
								
								html_resources/code-block/code-block.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								html_resources/code-block/code-block.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,122 @@
 | 
			
		|||
:host{
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
:host(.inline){
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
:host(.nowrap){
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
}
 | 
			
		||||
:host([data-name]){
 | 
			
		||||
  --copy-button-top: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
:host(.inline.copy-able){
 | 
			
		||||
  --copy-button-block-padding: 0;
 | 
			
		||||
  --codeblock-inline-padding: 40px;
 | 
			
		||||
}
 | 
			
		||||
caption{
 | 
			
		||||
  text-align: start;
 | 
			
		||||
  padding-inline-start: 3em;
 | 
			
		||||
}
 | 
			
		||||
caption:empty{
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
.outerBox{
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
table{
 | 
			
		||||
  border: 1px solid;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  border-spacing: 0px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin-inline-end: var(--codeblock-inline-padding,0);
 | 
			
		||||
}
 | 
			
		||||
tbody{
 | 
			
		||||
  counter-reset: nlines 0;
 | 
			
		||||
  color: lightblue;
 | 
			
		||||
  background-color: rgb(40,37,43);
 | 
			
		||||
}
 | 
			
		||||
@keyframes hide-show{
 | 
			
		||||
  0%{ opacity: 0 }
 | 
			
		||||
  10%{ opacity: 1 }
 | 
			
		||||
  90%{ opacity: 1 }
 | 
			
		||||
  100%{ opacity: 0 }
 | 
			
		||||
}
 | 
			
		||||
.copy-button::before{
 | 
			
		||||
  content: "Copy"
 | 
			
		||||
}
 | 
			
		||||
.copy-button.copy-success::before{
 | 
			
		||||
  white-space:nowrap;
 | 
			
		||||
  content: "Copied";
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  animation: hide-show 2s linear forwards;
 | 
			
		||||
}
 | 
			
		||||
.copy-button{
 | 
			
		||||
  padding: var(--copy-button-block-padding,0.3em) 0.3em;
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
  color: silver;
 | 
			
		||||
  border: 1px solid rgba(230,230,230,0.3);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  margin-top: var(--copy-button-top);
 | 
			
		||||
  right: .3em;
 | 
			
		||||
  cursor: pointer
 | 
			
		||||
}
 | 
			
		||||
.copy-button.copy-success,
 | 
			
		||||
.copy-button:hover{
 | 
			
		||||
  background-color: dodgerblue;
 | 
			
		||||
  color: black;
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.line-number{
 | 
			
		||||
  border-inline-end: 1px solid;
 | 
			
		||||
  background-color: rgba(0,0,0,0.1);
 | 
			
		||||
  color: silver;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  text-align: end;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (prefers-color-scheme: dark){
 | 
			
		||||
  tbody{
 | 
			
		||||
    /* We're cool and assume dark mode by default */
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
tr{
 | 
			
		||||
  counter-increment: nlines;
 | 
			
		||||
}
 | 
			
		||||
td,tr{
 | 
			
		||||
  border-block: none;
 | 
			
		||||
}
 | 
			
		||||
td{
 | 
			
		||||
  padding: 1px 0.7em;
 | 
			
		||||
}
 | 
			
		||||
.line-number::before{
 | 
			
		||||
  content: counter(nlines)
 | 
			
		||||
}
 | 
			
		||||
a{
 | 
			
		||||
  color: dodgerblue;
 | 
			
		||||
}
 | 
			
		||||
a:visited{
 | 
			
		||||
  color: orchid
 | 
			
		||||
}
 | 
			
		||||
tbody > div{ display: contents }
 | 
			
		||||
/* These should really be moved into separate highlighter css */
 | 
			
		||||
.comment{ color: rgb(50,180,90) }
 | 
			
		||||
.selector{ color: lavenderblush }
 | 
			
		||||
.pseudo{ color: rgb(200,180,250) }
 | 
			
		||||
.id{ color: rgb(240, 148, 138) }
 | 
			
		||||
.class{ color: aquamarine }
 | 
			
		||||
.attribute{ color: rgb(100,200,250) }  
 | 
			
		||||
.atrule{ color: lime }
 | 
			
		||||
.atvalue{ color: lightblue }
 | 
			
		||||
.property{ color: palegoldenrod }
 | 
			
		||||
.value{ color: skyblue }
 | 
			
		||||
.curly{ color: chocolate }
 | 
			
		||||
.function{ color: silver }
 | 
			
		||||
.important-tag{ color: orange }
 | 
			
		||||
.quote{ color: burlywood }
 | 
			
		||||
							
								
								
									
										400
									
								
								html_resources/code-block/code-block.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								html_resources/code-block/code-block.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,400 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
class CodeBlock extends HTMLElement{
 | 
			
		||||
  constructor(){
 | 
			
		||||
    super();
 | 
			
		||||
    let template = document.getElementById("code-block-template");
 | 
			
		||||
    let templateContent = template ? template.content : CodeBlock.Fragment();
 | 
			
		||||
    let cloned = templateContent.cloneNode(true);
 | 
			
		||||
    const shadowRoot = this.attachShadow({mode: 'open'})
 | 
			
		||||
    .appendChild(cloned);
 | 
			
		||||
    this.highlighter = {
 | 
			
		||||
      ready: false,
 | 
			
		||||
      waiting: false,
 | 
			
		||||
      fn: null,
 | 
			
		||||
      failed: false, 
 | 
			
		||||
      type: null,
 | 
			
		||||
      empty: true,
 | 
			
		||||
      linkGenerator: null,
 | 
			
		||||
      linkMatcher: null
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  determineAndLoadContent(){
 | 
			
		||||
    CodeBlock.getSource(this.src)
 | 
			
		||||
    .then(
 | 
			
		||||
      (data) => this.consumeData(data,CodeBlock.InsertMode.Replace),
 | 
			
		||||
      (e) => this.consumeData({content:this.textContent},CodeBlock.InsertMode.Replace)
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get name(){
 | 
			
		||||
    return this.dataset.name;
 | 
			
		||||
  }
 | 
			
		||||
  set name(some){
 | 
			
		||||
    this.dataset.name = some;
 | 
			
		||||
    this.shadowRoot.querySelector("caption").textContent = some;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  connectedCallback(){
 | 
			
		||||
    if(!this.isConnected || this.initialized){
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if(this.dataset.matchlinks){
 | 
			
		||||
      let parts = this.dataset.matchlinks.split(" -> ");
 | 
			
		||||
      // this is kinda sketchy
 | 
			
		||||
      if(parts.length === 2){
 | 
			
		||||
        try{
 | 
			
		||||
          this.highlighter.linkMatcher = new RegExp(parts[0],"g");
 | 
			
		||||
          this.highlighter.linkGenerator = (a) => (parts[1].replace("%s",a));
 | 
			
		||||
        }catch(e){
 | 
			
		||||
          console.warn(e);
 | 
			
		||||
          this.highlighter.linkMatcher = null;
 | 
			
		||||
          this.highlighter.linkGenerator = null;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if(this.dataset.name){
 | 
			
		||||
      this.name = this.dataset.name
 | 
			
		||||
    }
 | 
			
		||||
    this.initialized = true;
 | 
			
		||||
    
 | 
			
		||||
    if(this.copyable){
 | 
			
		||||
      CodeBlock.addClipboardListenerTo(this);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if(this.highlighter.empty && this.dataset.highlight){
 | 
			
		||||
      CodeBlock.addHighlighterTo(this);
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.determineAndLoadContent();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get copyable(){
 | 
			
		||||
    return this.classList.contains("copy-able")
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static addHighlighterTo(elem){
 | 
			
		||||
    if(elem instanceof CodeBlock){
 | 
			
		||||
      elem.highlighter.empty = false;
 | 
			
		||||
      switch(elem.dataset.highlight){
 | 
			
		||||
        case "css":
 | 
			
		||||
        case "simple":
 | 
			
		||||
          elem.highlighter.type = elem.dataset.highlight;
 | 
			
		||||
          elem.highlighter.waiting = true;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          console.warn("invalid highlighter");
 | 
			
		||||
          elem.determineAndLoadContent();
 | 
			
		||||
          return
 | 
			
		||||
      }
 | 
			
		||||
      import("./code-block-highlighter.js")
 | 
			
		||||
      .then(it => {
 | 
			
		||||
        switch(elem.highlighter.type){
 | 
			
		||||
          case "css":
 | 
			
		||||
            elem.highlighter.fn = new it.CSSHighlighter();
 | 
			
		||||
            break;
 | 
			
		||||
          case "simple":
 | 
			
		||||
            elem.highlighter.fn = new it.SimpleHighlighter();
 | 
			
		||||
        }
 | 
			
		||||
        elem.highlighter.ready = true;
 | 
			
		||||
        elem.highlighter.waiting = false;
 | 
			
		||||
        elem.determineAndLoadContent()
 | 
			
		||||
      })
 | 
			
		||||
      .catch(e => {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
        elem.highlighter.failed = true;
 | 
			
		||||
        ele.highlighter.waiting = false;
 | 
			
		||||
        elem.determineAndLoadContent()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  clearContent(){
 | 
			
		||||
    let innerbox = this.codeBox;
 | 
			
		||||
    while(innerbox.children.length){
 | 
			
		||||
      innerbox.children[0].remove();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static addClipboardListenerTo(aBlock){
 | 
			
		||||
    let copyButton = aBlock.copyButton;
 | 
			
		||||
    if(copyButton){
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    copyButton = aBlock.shadowRoot.querySelector(".copy-button");
 | 
			
		||||
    aBlock.copyButton = copyButton;
 | 
			
		||||
   
 | 
			
		||||
    copyButton.addEventListener("click",(e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      try{
 | 
			
		||||
        let writing = navigator.clipboard.writeText(aBlock.value);
 | 
			
		||||
        writing.then(()=>{
 | 
			
		||||
          copyButton.classList.add("copy-success");
 | 
			
		||||
          setTimeout(()=>copyButton.classList.remove("copy-success"),2000);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
      }catch(e){
 | 
			
		||||
        console.error("couldn't copy content to clipboard");
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    });
 | 
			
		||||
    aBlock.copyButton.removeAttribute("hidden");
 | 
			
		||||
  }
 | 
			
		||||
  static getSource(some){
 | 
			
		||||
    return new Promise((res, rej) => {
 | 
			
		||||
      if(some && typeof some === "string"){
 | 
			
		||||
        CodeBlock.TryLoadFile(some)
 | 
			
		||||
        .then(res)
 | 
			
		||||
        .catch((e)=>{
 | 
			
		||||
          console.error(e);
 | 
			
		||||
          rej(e)
 | 
			
		||||
        })
 | 
			
		||||
      }else{
 | 
			
		||||
        setTimeout(()=>rej("argument must be a string"));
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async setSource(some){
 | 
			
		||||
    this.clearContent();
 | 
			
		||||
    let res = await CodeBlock.getSource(some);
 | 
			
		||||
    if(res.ok){
 | 
			
		||||
      this.consumeData(res,CodeBlock.InsertMode.Replace);
 | 
			
		||||
    }
 | 
			
		||||
    return { ok: res.ok }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get src(){
 | 
			
		||||
    return this.getAttribute("src")
 | 
			
		||||
  }
 | 
			
		||||
  set src(some){
 | 
			
		||||
    this.setSource(some);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  lines(){
 | 
			
		||||
    const lines = this.codeBox.querySelectorAll("tr");
 | 
			
		||||
    const lineCount = lines.length;
 | 
			
		||||
    let currentLine = 0;
 | 
			
		||||
    return {
 | 
			
		||||
      next: function() {
 | 
			
		||||
          return currentLine < lineCount ? {
 | 
			
		||||
            value: lines[currentLine++],
 | 
			
		||||
            done: false
 | 
			
		||||
          } : { done: true }
 | 
			
		||||
      },
 | 
			
		||||
      [Symbol.iterator]: function() { return this; }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  getNamedSection(name){
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    let sections = this.codeBox.children;
 | 
			
		||||
    while(i < sections.length){
 | 
			
		||||
      if(sections[i].dataset.name === name){
 | 
			
		||||
        return sections[i]
 | 
			
		||||
      }
 | 
			
		||||
      i++
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async consumeData(some,insertMode){
 | 
			
		||||
    const re = /.*\r?\n/g;
 | 
			
		||||
    if(typeof some.content !== "string"){
 | 
			
		||||
      some.content = some.content.toString();
 | 
			
		||||
    }
 | 
			
		||||
    this.textContent = "";
 | 
			
		||||
    
 | 
			
		||||
    let innerbox = this.codeBox;
 | 
			
		||||
    
 | 
			
		||||
    if(innerbox.children.length === 1 && innerbox.firstChild.textContent === ""){
 | 
			
		||||
      insertMode = CodeBlock.InsertMode.Replace;
 | 
			
		||||
    }
 | 
			
		||||
    const INSERT_MODE = insertMode || some.insertMode || CodeBlock.InsertMode.Append;
 | 
			
		||||
    
 | 
			
		||||
    const aDiv = document.createElement("div");
 | 
			
		||||
    if(some.name){
 | 
			
		||||
      aDiv.setAttribute("data-name",some.name);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const hasHighlighter = this.highlighter.ready;
 | 
			
		||||
    const LIMIT = 10000; // Arbitrary limit of 10k lines
 | 
			
		||||
    
 | 
			
		||||
    if(hasHighlighter){
 | 
			
		||||
      this.highlighter.fn.reset();
 | 
			
		||||
      const payload = {
 | 
			
		||||
        "match" : re.exec(some.content),
 | 
			
		||||
        "linkMatcher": this.highlighter.linkMatcher,
 | 
			
		||||
        "linkGenerator": this.highlighter.linkGenerator,
 | 
			
		||||
        "linkChanged": true,
 | 
			
		||||
      };
 | 
			
		||||
      Object.defineProperty(payload,"content",{get:()=>payload.match[0]});
 | 
			
		||||
      let counter = 0;
 | 
			
		||||
      let lastIdx = 0;
 | 
			
		||||
      
 | 
			
		||||
      while(payload.match && (counter++ < LIMIT)){
 | 
			
		||||
        aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
 | 
			
		||||
        this.highlighter.fn.parse(
 | 
			
		||||
          payload,
 | 
			
		||||
          aDiv.lastElementChild.lastChild
 | 
			
		||||
        );
 | 
			
		||||
        payload.linkChanged = false;
 | 
			
		||||
        lastIdx = (payload.match.index + payload.match[0].length);
 | 
			
		||||
        payload.match = re.exec(some.content);
 | 
			
		||||
      }
 | 
			
		||||
      // Handle case where the content does not end with newline
 | 
			
		||||
      aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
 | 
			
		||||
      if(lastIdx < some.content.length){
 | 
			
		||||
        payload.match = [some.content.slice(lastIdx)];
 | 
			
		||||
        this.highlighter.fn.parse(
 | 
			
		||||
          payload,
 | 
			
		||||
          aDiv.lastElementChild.lastChild
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }else{
 | 
			
		||||
      let match = re.exec(some.content);
 | 
			
		||||
      let counter = 0;
 | 
			
		||||
      let lastIdx = 0;
 | 
			
		||||
      
 | 
			
		||||
      while(match && (counter++ < LIMIT)){
 | 
			
		||||
        aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
 | 
			
		||||
        aDiv.lastElementChild.lastChild.textContent = match[0];
 | 
			
		||||
        lastIdx = (match.index + match[0].length);
 | 
			
		||||
        match = re.exec(some.content);
 | 
			
		||||
      }
 | 
			
		||||
      // Handle case where the content does not end with newline
 | 
			
		||||
      aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
 | 
			
		||||
      if(lastIdx < some.content.length){
 | 
			
		||||
        aDiv.lastElementChild.lastChild.textContent = some.content.slice(lastIdx);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    switch(INSERT_MODE){
 | 
			
		||||
      case CodeBlock.InsertMode.Prepend:
 | 
			
		||||
        aDiv.lastElementChild.lastElementChild.append("\n")
 | 
			
		||||
        innerbox.insertBefore(aDiv,innerbox.firstChild);
 | 
			
		||||
        break;
 | 
			
		||||
      case CodeBlock.InsertMode.Replace:
 | 
			
		||||
        this.clearContent();
 | 
			
		||||
      case CodeBlock.InsertMode.Append:
 | 
			
		||||
        // Push the first "line" of new section to the last line of old content, if old content exists
 | 
			
		||||
        if(innerbox.lastElementChild){
 | 
			
		||||
          let first = aDiv.firstChild.lastElementChild;
 | 
			
		||||
          let lastRowContent = this.lastContentLine;
 | 
			
		||||
          for(let one of Array.from(first.childNodes)){
 | 
			
		||||
            lastRowContent.appendChild(one)
 | 
			
		||||
          }
 | 
			
		||||
          aDiv.firstChild.remove();
 | 
			
		||||
        }
 | 
			
		||||
        if(aDiv.children.length){
 | 
			
		||||
          innerbox.appendChild(aDiv);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case CodeBlock.InsertMode.AppendLines:
 | 
			
		||||
        if(aDiv.children.length){
 | 
			
		||||
          let lastRowContent = this.lastContentLine;
 | 
			
		||||
          if(lastRowContent){
 | 
			
		||||
            lastRowContent.append("\n");
 | 
			
		||||
          }
 | 
			
		||||
          innerbox.appendChild(aDiv);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        console.warn("unimplemented insertMode")
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get lastContentLine(){
 | 
			
		||||
    return this.codeBox.lastElementChild?.lastChild.lastChild;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get codeBox(){
 | 
			
		||||
    return this.shadowRoot.querySelector("tbody");
 | 
			
		||||
  }
 | 
			
		||||
  get value(){
 | 
			
		||||
    return this.codeBox.textContent
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get InsertModes(){
 | 
			
		||||
    return CodeBlock.InsertMode
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  set value(thing){
 | 
			
		||||
    if(typeof thing === "string"){
 | 
			
		||||
      this.consumeData({content:thing,insertMode:CodeBlock.InsertMode.Replace});
 | 
			
		||||
    }else if("content" in thing){
 | 
			
		||||
      this.consumeData(thing,CodeBlock.InsertMode.Replace);
 | 
			
		||||
    }else{
 | 
			
		||||
      this.consumeData({content: thing.toString(), insertMode: CodeBlock.InsertMode.Replace});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  insertContent(thing){
 | 
			
		||||
    if(typeof thing === "string"){
 | 
			
		||||
      this.consumeData({content:thing});
 | 
			
		||||
    }else if("content" in thing){
 | 
			
		||||
      this.consumeData(thing);
 | 
			
		||||
    }else{
 | 
			
		||||
      this.consumeData({content: thing.toString()});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static InsertMode = {
 | 
			
		||||
    Replace : Symbol("replace"),
 | 
			
		||||
    Append : Symbol("append"),
 | 
			
		||||
    AppendLines : Symbol("appendlines"),
 | 
			
		||||
    Prepend : Symbol("prepend")
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static async TryLoadFile(name){
 | 
			
		||||
    let response = await fetch(name);
 | 
			
		||||
    if(response.ok){
 | 
			
		||||
      let content = await response.text();
 | 
			
		||||
      return { content: content, ok: true }
 | 
			
		||||
    }else{
 | 
			
		||||
      throw {error: "no response", ok: false }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static RowFragment = (() => {
 | 
			
		||||
    let frag = new DocumentFragment();
 | 
			
		||||
    let tr = frag.appendChild(document.createElement("tr"));
 | 
			
		||||
    tr.appendChild(document.createElement("td"));
 | 
			
		||||
    tr.firstChild.setAttribute("class","line-number");
 | 
			
		||||
    tr.appendChild(document.createElement("td"));
 | 
			
		||||
    return frag
 | 
			
		||||
  })();
 | 
			
		||||
  
 | 
			
		||||
  static Fragment(){
 | 
			
		||||
    let frag = new DocumentFragment();
 | 
			
		||||
    let link = document.createElement("link");
 | 
			
		||||
    link.setAttribute("as","style");
 | 
			
		||||
    link.setAttribute("type","text/css");
 | 
			
		||||
    link.setAttribute("rel","preload prefetch stylesheet");
 | 
			
		||||
    // Change the relative stylesheet address here if required
 | 
			
		||||
    link.setAttribute("href","html_resources/code-block/code-block.css");
 | 
			
		||||
    frag.appendChild(link);
 | 
			
		||||
    let outerBox = frag.appendChild(document.createElement("div"));
 | 
			
		||||
    outerBox.setAttribute("part","outerBox");
 | 
			
		||||
    outerBox.className = "outerBox";
 | 
			
		||||
    let copyButton = outerBox.appendChild(document.createElement("div"));
 | 
			
		||||
    copyButton.setAttribute("part","copyButton");
 | 
			
		||||
    copyButton.className = "copy-button";
 | 
			
		||||
    copyButton.setAttribute("hidden",true);
 | 
			
		||||
    copyButton.setAttribute("role","button");
 | 
			
		||||
    let table = document.createElement("table");
 | 
			
		||||
    let caption = table.appendChild(document.createElement("caption"));
 | 
			
		||||
    caption.setAttribute("part","title");
 | 
			
		||||
    let content = table.appendChild(document.createElement("tbody"));
 | 
			
		||||
    content.setAttribute("part","content");
 | 
			
		||||
    outerBox.appendChild(table)
 | 
			
		||||
 | 
			
		||||
    return frag
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("code-block",CodeBlock);
 | 
			
		||||
| 
						 | 
				
			
			@ -55,14 +55,16 @@ pre:empty{ display: none }
 | 
			
		|||
  white-space:nowrap;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
.target > a{
 | 
			
		||||
  display:inline-block;
 | 
			
		||||
  width: 1.2em;
 | 
			
		||||
  height: 80%;
 | 
			
		||||
  height: 1.2em;
 | 
			
		||||
  background-image: url("ext.svg");
 | 
			
		||||
  background-repeat: no-repeat;
 | 
			
		||||
  background-size: contain;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
.target:hover{ background-color: rgb(80,80,90) }
 | 
			
		||||
.target.selected{ background-color: rgb(80,80,120) }
 | 
			
		||||
| 
						 | 
				
			
			@ -92,20 +94,6 @@ pre > div{
 | 
			
		|||
.categoryList:not(.blurred)+.categoryList,
 | 
			
		||||
.hidden{ display: none !important }
 | 
			
		||||
 | 
			
		||||
.comment{ color: rgb(50,180,90) }
 | 
			
		||||
.selector{ color: lavenderblush }
 | 
			
		||||
.pseudo{ color: rgb(200,180,250) }
 | 
			
		||||
.id{ color: rgb(240, 148, 138) }
 | 
			
		||||
.class{ color: skyblue }
 | 
			
		||||
.attribute{ color: rgb(120,230,170) }  
 | 
			
		||||
.atrule{ color: lime }
 | 
			
		||||
.atvalue{ color: lightblue }
 | 
			
		||||
.property{ color: palegoldenrod }
 | 
			
		||||
.value{ color: skyblue }
 | 
			
		||||
.curly{ color: magenta }
 | 
			
		||||
.function{ color: silver }
 | 
			
		||||
.important-tag{ color: orange }
 | 
			
		||||
 | 
			
		||||
@keyframes showDelayed{ from{ visibility: hidden } to{ visibility: visibile }}
 | 
			
		||||
 | 
			
		||||
@keyframes loadingBar{ from{ background-size: 0% } to{ background-size: 100% } }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ function fetchWithType(url){
 | 
			
		|||
        .then((obj) => resolve({file:url,content:obj}));
 | 
			
		||||
      }else{
 | 
			
		||||
        response.text()
 | 
			
		||||
        .then((text) => resolve({file:url,content:text}))
 | 
			
		||||
        .then((text) => resolve({file:url,content:text,name:url}))
 | 
			
		||||
        
 | 
			
		||||
      }
 | 
			
		||||
    },except => reject(except))
 | 
			
		||||
| 
						 | 
				
			
			@ -103,14 +103,6 @@ function getSecondaryCategories(list){
 | 
			
		|||
  return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clearCodeBlock(){
 | 
			
		||||
  const pre = document.getElementById("previewBox");
 | 
			
		||||
  for(let el of Array.from(pre.childNodes)){
 | 
			
		||||
    pre.removeChild(el)
 | 
			
		||||
  }
 | 
			
		||||
  return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showMatchingTargets(fileNames,setSelected = false){
 | 
			
		||||
  let bonus = 0;
 | 
			
		||||
  for(let c of Array.from(document.querySelectorAll(".target"))){
 | 
			
		||||
| 
						 | 
				
			
			@ -152,13 +144,27 @@ function onCategoryClicked(categoryNode,isSecondary = false){
 | 
			
		|||
  return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function onTargetClicked(target,append = false){
 | 
			
		||||
  const text = typeof target === "string"
 | 
			
		||||
              ? target
 | 
			
		||||
              : getText(target);
 | 
			
		||||
  
 | 
			
		||||
  fetchWithType(`chrome/${text}`)
 | 
			
		||||
  .then(obj => Highlighter.parse(obj,append))
 | 
			
		||||
  .then(obj => {
 | 
			
		||||
    let box = document.getElementById("previewBox");
 | 
			
		||||
    if(append){
 | 
			
		||||
      if(obj.file.endsWith("window_control_placeholder_support.css")){
 | 
			
		||||
        obj.insertMode = box.InsertModes.Prepend;
 | 
			
		||||
        box.insertContent(obj);
 | 
			
		||||
      }else{
 | 
			
		||||
        obj.insertMode = box.InsertModes.AppendLines;
 | 
			
		||||
        box.insertContent(obj);
 | 
			
		||||
      }
 | 
			
		||||
    }else{
 | 
			
		||||
      box.value = obj
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  .catch(e => console.log(e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -178,7 +184,8 @@ function onFilenameClicked(box,ctrlKey){
 | 
			
		|||
  }else{
 | 
			
		||||
    if(ctrlKey){
 | 
			
		||||
      selectedTarget.deselect(box);
 | 
			
		||||
      let preview = document.querySelector(`[data-filename="chrome/${box.getAttribute("title")}.css"]`);
 | 
			
		||||
      let previewbox = document.getElementById("previewBox");
 | 
			
		||||
      let preview = previewbox.getNamedSection(`chrome/${box.getAttribute("title")}.css`);
 | 
			
		||||
      if(preview){
 | 
			
		||||
        preview.remove();
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -309,260 +316,6 @@ function createCategories(){
 | 
			
		|||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Highlighter = new(function(){
 | 
			
		||||
 | 
			
		||||
  const state = new (function(){
 | 
			
		||||
    let current = 0;
 | 
			
		||||
    let previous = 0;
 | 
			
		||||
    this.now = ()=>current;
 | 
			
		||||
    this.previous = ()=>previous;
 | 
			
		||||
    this.set = function(a){ previous = current; current = a; return} 
 | 
			
		||||
  })();
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
  let pointer = 0;
 | 
			
		||||
  let token = "";
 | 
			
		||||
  
 | 
			
		||||
  const selectorToClassMap = new Map([
 | 
			
		||||
  [":","pseudo"],
 | 
			
		||||
  ["#","id"],
 | 
			
		||||
  [".","class"],
 | 
			
		||||
  ["[","attribute"]]);
 | 
			
		||||
 | 
			
		||||
  this.parse = function(info,appendMode){
 | 
			
		||||
    
 | 
			
		||||
    const targetNode = document.getElementById("previewBox");
 | 
			
		||||
    
 | 
			
		||||
    !appendMode && clearCodeBlock();
 | 
			
		||||
    
 | 
			
		||||
    let node = document.createElement("div");
 | 
			
		||||
    node.setAttribute("data-filename",info.file);
 | 
			
		||||
    
 | 
			
		||||
    function createNewRuleset(){
 | 
			
		||||
      let ruleset = document.createElement("span");
 | 
			
		||||
      ruleset.className = "ruleset";
 | 
			
		||||
      node.appendChild(ruleset);
 | 
			
		||||
      return ruleset
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let rulesetUnderConstruction = createNewRuleset();
 | 
			
		||||
 | 
			
		||||
    function createElementFromToken(type,c){
 | 
			
		||||
      if(token.length === 0 && !c){
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      let n = document.createElement("span");
 | 
			
		||||
      
 | 
			
		||||
      switch(type){
 | 
			
		||||
        case "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 = token.split(/([\.#:\[]\w[\w-_"'=\]]*|\s\w[\w-_"'=\]]*)/);
 | 
			
		||||
        
 | 
			
		||||
          for(let part of parts){
 | 
			
		||||
            if(part.length === 0){
 | 
			
		||||
              continue
 | 
			
		||||
            }
 | 
			
		||||
            let c = part[0];
 | 
			
		||||
            switch (c){
 | 
			
		||||
              case ":":
 | 
			
		||||
              case "#":
 | 
			
		||||
              case "[":
 | 
			
		||||
              case ".":
 | 
			
		||||
                let p = n.appendChild(document.createElement("span"));
 | 
			
		||||
                p.className = selectorToClassMap.get(c);
 | 
			
		||||
                p.textContent = part;
 | 
			
		||||
                break;
 | 
			
		||||
              default:
 | 
			
		||||
                n.append(part);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          break
 | 
			
		||||
        case "comment":
 | 
			
		||||
          let linksToFile = token.match(/[\w-\.]+\.css/g);
 | 
			
		||||
          if(linksToFile && linksToFile.length){
 | 
			
		||||
            let linkIdx = 0;
 | 
			
		||||
            let fromIdx = 0;
 | 
			
		||||
            while(linkIdx < linksToFile.length){
 | 
			
		||||
              let part = linksToFile[linkIdx++];
 | 
			
		||||
              let idx = token.indexOf(part);
 | 
			
		||||
              n.append(token.substring(fromIdx,idx));
 | 
			
		||||
              let link = document.createElement("a");
 | 
			
		||||
              link.textContent = part;
 | 
			
		||||
              link.href = `https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/${part}`;
 | 
			
		||||
              link.target = "_blank";
 | 
			
		||||
              n.appendChild(link);
 | 
			
		||||
              fromIdx = idx + part.length;
 | 
			
		||||
            }
 | 
			
		||||
            n.append(token.substring(fromIdx));
 | 
			
		||||
          }else{
 | 
			
		||||
            n.textContent = c || token;
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case "value":
 | 
			
		||||
          let startImportant = token.indexOf("!");
 | 
			
		||||
          if(startImportant === -1){
 | 
			
		||||
            n.textContent = c || token;
 | 
			
		||||
          }else{
 | 
			
		||||
            n.textContent = token.substr(0,startImportant);
 | 
			
		||||
            let importantTag = document.createElement("span");
 | 
			
		||||
            importantTag.className = "important-tag";
 | 
			
		||||
            importantTag.textContent = "!important";
 | 
			
		||||
            n.appendChild(importantTag);
 | 
			
		||||
            if(token.length > (9 + startImportant)){
 | 
			
		||||
              n.append(";")
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case "function":
 | 
			
		||||
          n.textContent = c || token.slice(0,-1);
 | 
			
		||||
          break
 | 
			
		||||
        default:
 | 
			
		||||
          n.textContent = c || token;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      n.className = (`token ${type}`);
 | 
			
		||||
      token = "";
 | 
			
		||||
      rulesetUnderConstruction.appendChild(n);
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let c;
 | 
			
		||||
    let functionValueLevel = 0;
 | 
			
		||||
    let curly = false;
 | 
			
		||||
    
 | 
			
		||||
    while(pointer < info.content.length){
 | 
			
		||||
      c = info.content[pointer];
 | 
			
		||||
      
 | 
			
		||||
      const currentState = state.now();
 | 
			
		||||
      curly = currentState != 2 && (c === "{" || c === "}");
 | 
			
		||||
      if(!curly){
 | 
			
		||||
        token+=c;
 | 
			
		||||
      }
 | 
			
		||||
      switch(currentState){
 | 
			
		||||
      
 | 
			
		||||
        case 0:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case "/":
 | 
			
		||||
              if(info.content[pointer+1] === "*"){
 | 
			
		||||
                state.set(2);
 | 
			
		||||
                if(token.length > 1){
 | 
			
		||||
                  token = token.slice(0,-1);
 | 
			
		||||
                  createElementFromToken("selector");
 | 
			
		||||
                  token += "/"
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            case "{":
 | 
			
		||||
              state.set(3);
 | 
			
		||||
              createElementFromToken("selector");
 | 
			
		||||
              break;
 | 
			
		||||
            case "}":
 | 
			
		||||
              createElementFromToken("text");
 | 
			
		||||
              break;
 | 
			
		||||
            case "@":
 | 
			
		||||
              state.set(5);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          break;
 | 
			
		||||
      
 | 
			
		||||
        case 2:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case "*":
 | 
			
		||||
              if(info.content[pointer+1] === "/"){
 | 
			
		||||
                token += "/";
 | 
			
		||||
                pointer++;
 | 
			
		||||
                state.set(state.previous());
 | 
			
		||||
                createElementFromToken("comment");
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case 3:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case "/":
 | 
			
		||||
              if(info.content[pointer+1] === "*"){
 | 
			
		||||
                state.set(2);
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            case ":":
 | 
			
		||||
              createElementFromToken("property");
 | 
			
		||||
              state.set(4);
 | 
			
		||||
              break;
 | 
			
		||||
            case "}":
 | 
			
		||||
              createElementFromToken("text");
 | 
			
		||||
              state.set(0);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 4:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case ";":
 | 
			
		||||
              createElementFromToken("value");
 | 
			
		||||
              state.set(3);
 | 
			
		||||
              break;
 | 
			
		||||
            case "}":
 | 
			
		||||
              createElementFromToken("value");
 | 
			
		||||
              state.set(0);
 | 
			
		||||
              break;
 | 
			
		||||
            case "(":
 | 
			
		||||
              createElementFromToken("value");
 | 
			
		||||
              functionValueLevel++;
 | 
			
		||||
              state.set(7);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 5:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case " ":
 | 
			
		||||
              createElementFromToken("atrule");
 | 
			
		||||
              state.set(6);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 6:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case ";":
 | 
			
		||||
            case "{":
 | 
			
		||||
              createElementFromToken("atvalue");
 | 
			
		||||
              state.set(0);
 | 
			
		||||
          }
 | 
			
		||||
          break
 | 
			
		||||
        case 7:
 | 
			
		||||
          switch(c){
 | 
			
		||||
            case ")":
 | 
			
		||||
              functionValueLevel--;
 | 
			
		||||
              if(functionValueLevel === 0){
 | 
			
		||||
                createElementFromToken("function");
 | 
			
		||||
                token = ")";
 | 
			
		||||
                state.set(4);
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            case "}":
 | 
			
		||||
              functionValueLevel = 0;
 | 
			
		||||
              state.set(0)
 | 
			
		||||
          }
 | 
			
		||||
        default:
 | 
			
		||||
          false
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      curly && createElementFromToken("curly",c);
 | 
			
		||||
      
 | 
			
		||||
      pointer++
 | 
			
		||||
    }
 | 
			
		||||
    createElementFromToken("text");
 | 
			
		||||
    token = "";
 | 
			
		||||
    state.set(0);
 | 
			
		||||
    pointer = 0;
 | 
			
		||||
    
 | 
			
		||||
    if(info.file.endsWith("support.css")){
 | 
			
		||||
      targetNode.prepend(node);
 | 
			
		||||
    }else{
 | 
			
		||||
      targetNode.appendChild(node);
 | 
			
		||||
    }
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  return this
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
async function handleSearchQuery(){
 | 
			
		||||
  let params = (new URL(document.location)).searchParams;
 | 
			
		||||
  let param = params.get("tag");
 | 
			
		||||
| 
						 | 
				
			
			@ -579,7 +332,7 @@ async function handleSearchQuery(){
 | 
			
		|||
  param = params.get("file");
 | 
			
		||||
  if(param){
 | 
			
		||||
    let files = param.split(",").filter(a => DB.keys.includes(a));
 | 
			
		||||
    
 | 
			
		||||
    let box = document.getElementById("previewBox");
 | 
			
		||||
    if(files.length === 0 ){
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -589,7 +342,10 @@ async function handleSearchQuery(){
 | 
			
		|||
    Promise.all(promises)
 | 
			
		||||
    .then(responses => {
 | 
			
		||||
      showMatchingTargets(files,true);
 | 
			
		||||
      responses.forEach(Highlighter.parse)
 | 
			
		||||
      responses.forEach((res)=>{
 | 
			
		||||
        res.insertMode = res.file.endsWith("window_control_placeholder_support.css") ? box.InsertModes.Prepend : box.InsertModes.AppendLines;
 | 
			
		||||
        box.insertContent(res)
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -611,8 +367,8 @@ document.onreadystatechange = (function () {
 | 
			
		|||
  
 | 
			
		||||
  if (document.readyState === "complete") {
 | 
			
		||||
    function linkClicked(ev){
 | 
			
		||||
      if(ev.target instanceof HTMLAnchorElement){
 | 
			
		||||
        let ref = ev.target.href;
 | 
			
		||||
      if(ev.originalTarget instanceof HTMLAnchorElement){
 | 
			
		||||
        let ref = ev.originalTarget.href;
 | 
			
		||||
        if(!ref){
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@
 | 
			
		|||
  <meta name="viewport" content="width=device-width"/>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <title>firefox-csshacks tag browser</title>
 | 
			
		||||
  <script type="text/javascript" src="html_resources/selector.js"></script>
 | 
			
		||||
  <script type="module" src="html_resources/selector.js"></script>
 | 
			
		||||
  <script type="module" src="html_resources/code-block/code-block.js"></script>
 | 
			
		||||
  <link href="html_resources/main.css" rel="stylesheet" type="text/css"/>
 | 
			
		||||
  <link rel="icon" href="html_resources/favicon.svg" type="image/svg+xml">
 | 
			
		||||
</head>
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +22,7 @@
 | 
			
		|||
    <div id="targets">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
      <pre id="previewBox"></pre>
 | 
			
		||||
      <code-block data-highlight="css" data-matchlinks="[\w-\.]+\.css -> ./chrome/%s" id="previewBox" class="copy-able"></code-block>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue