diff --git a/html_resources/code-block/code-block-highlighter.js b/html_resources/code-block/code-block-highlighter.js new file mode 100644 index 0000000..c5511ef --- /dev/null +++ b/html_resources/code-block/code-block-highlighter.js @@ -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 } \ No newline at end of file diff --git a/html_resources/code-block/code-block.css b/html_resources/code-block/code-block.css new file mode 100644 index 0000000..2554237 --- /dev/null +++ b/html_resources/code-block/code-block.css @@ -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 } \ No newline at end of file diff --git a/html_resources/code-block/code-block.js b/html_resources/code-block/code-block.js new file mode 100644 index 0000000..73c3095 --- /dev/null +++ b/html_resources/code-block/code-block.js @@ -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); diff --git a/html_resources/main.css b/html_resources/main.css index cadcfaf..a222f9c 100644 --- a/html_resources/main.css +++ b/html_resources/main.css @@ -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% } } diff --git a/html_resources/selector.js b/html_resources/selector.js index 8a0d42e..21bfcee 100644 --- a/html_resources/selector.js +++ b/html_resources/selector.js @@ -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 } diff --git a/index.html b/index.html index 8c935a1..5f37231 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,8 @@ firefox-csshacks tag browser - + + @@ -21,7 +22,7 @@
-

+