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…
Reference in a new issue