tag-browser: use separate <code-block> module for style preview

This commit is contained in:
MrOtherGuy 2022-04-03 15:01:07 +03:00
parent b9e140dbbd
commit 97e42e134a
6 changed files with 991 additions and 286 deletions

View 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 }

View 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 }

View 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);

View file

@ -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% } }

View file

@ -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
}

View file

@ -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>