Highlight Web 2.0
July 17, 2024
Following my old post, i realized that highlighting content, creating a new file and copying everything was too slow when im in a marathon reading.
Thats how this 2.0 version was born. Inspired by Going Lean by Lea Verou i started looking if i can do something similar but the other way around. Prepopulate a new file in github with the content of the current article. I found a github issue that answers this.
So the only thing left was extend my previous bookmarklet to also save the selected text and make some ui with two buttons: one for highlighting, and one for sharing.
For the ui, nothing fancy, just a wrapper div with two buttons.
let $wrapper, $buttonHighlight;
function buildUI(){
$wrapper = document.querySelector(".pudymody-highlighter")
if( $wrapper !== null ){
return;
}
$wrapper = document.createElement("div");
$wrapper.className = "pudymody-highlighter"
$buttonHighlight = document.createElement("button");
$buttonHighlight.style = `
all: unset;
padding: 0.5rem;
background: #000;
color: #fff;
border-radius: 50%;
cursor: pointer;
display:block;
user-select:none;
position: fixed;
bottom: 0.5rem;
left: 0.5rem;
`;
$buttonHighlight.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:1rem;height:1rem;display:block;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
</svg>`
$buttonHighlight.addEventListener("click", highlightText);
$wrapper.appendChild($buttonHighlight);
$buttonShare = document.createElement("button");
$buttonShare.style = `
all: unset;
padding: 0.5rem;
background: #000;
color: #fff;
border-radius: 50%;
cursor: pointer;
display:block;
user-select:none;
position: fixed;
bottom: 0.5rem;
left: 3rem;
`;
$buttonShare.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:1rem;height:1rem;display:block;">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
</svg>`
$buttonShare.addEventListener("click", share);
$wrapper.appendChild($buttonShare);
document.body.appendChild($wrapper);
}
To save the selected texts, its almost the same as the previous one, but also using the fabolous cloneContents of the Range object.
let selections = [];
function highlightText(){
const currentSelection = window.getSelection();
const totalSelections = currentSelection.rangeCount;
for(let i = 0; i < totalSelections; i++){
const range = currentSelection.getRangeAt(i);
selections.push(
range.cloneContents()
);
[...range.getClientRects()]
.map( e => {
const a = document.createElement("mark");
a.style.position = "absolute";
a.style.pointerEvents = "none";
a.style.background = highlightColor;
a.style.left= (e.left+document.documentElement.scrollLeft)+"px";
a.style.top = (e.top+document.documentElement.scrollTop)+"px";
a.style.height = e.height+"px";
a.style.width = e.width+"px";
return a;
})
.forEach(d => { $wrapper.appendChild(d); });
}
}
And finally, to convert the html to markdown, im using the unified library with the rehype-remark plugin. Which is an ecosystem of tools to work with syntax trees. Im also using the amazing ESM cdn to convert this libraries to ESM ones.
const {unified} = await import('https://esm.sh/unified?exports=unified')
const {default: rehypeParse} = await import('https://esm.sh/rehype-parse')
const {default: remarkStringify} = await import('https://esm.sh/remark-stringify')
const {default: rehypeRemark} = await import('https://esm.sh/rehype-remark')
const processor = unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkStringify);
function getFileName(){
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = (date.getDate()).toString().padStart(2, "0");
const hour = (date.getHours()).toString().padStart(2, "0");
const minutes = (date.getMinutes()).toString().padStart(2, "0");
return `${year}-${month}-${day}_${hour}-${minutes}.md`
}
function share(){
const fileName = getFileName();
const htmlBody = selections
.flatMap( e => [...e.childNodes] )
.map( e => e.nodeType === e.TEXT_NODE ? `<p>${e.textContent}</p>` : e.outerHTML)
.join("");
const markdownTxt = processor.processSync(htmlBody)
.value
.split("\n\n")
.map( l => `> ${l}`)
markdownTxt.push(`From [${document.title}](${location.toString()})`)
markdownTxt.unshift(`---
layout: "post"
date: ${formatISO(new Date())}
---`);
fileContent = markdownTxt.join("\n\n");
window.open(`https://github.com/pudymody/pudymody.github.io/new/master/content/stream?filename=${encodeURIComponent(fileName)}&value=${encodeURIComponent(fileContent)}`);
}
Code
And finally, all the code together and as a bookmarklet for your use:
(async function(window, document){
/* esm.sh - esbuild bundle(date-fns@3.6.0/formatISO) es2022 production */
function p(t){let n=Object.prototype.toString.call(t);return t instanceof Date||typeof t=="object"&&n==="[object Date]"?new t.constructor(+t):typeof t=="number"||n==="[object Number]"||typeof t=="string"||n==="[object String]"?new Date(t):new Date(NaN)}function o(t,n){let e=t<0?"-":"",i=Math.abs(t).toString().padStart(n,"0");return e+i}function formatISO(t,n){let e=p(t);if(isNaN(e.getTime()))throw new RangeError("Invalid time value");let i=n?.format??"extended",f=n?.representation??"complete",s="",c="",d=i==="extended"?"-":"",m=i==="extended"?":":"";if(f!=="time"){let r=o(e.getDate(),2),a=o(e.getMonth()+1,2);s=`${o(e.getFullYear(),4)}${d}${a}${d}${r}`}if(f!=="date"){let r=e.getTimezoneOffset();if(r!==0){let u=Math.abs(r),D=o(Math.trunc(u/60),2),h=o(u%60,2);c=`${r<0?"+":"-"}${D}:${h}`}else c="Z";let a=o(e.getHours(),2),l=o(e.getMinutes(),2),g=o(e.getSeconds(),2),$=s===""?"":"T",b=[a,l,g].join(m);s=`${s}${$}${b}${c}`}return s};
const {unified} = await import('https://esm.sh/unified?exports=unified')
const {default: rehypeParse} = await import('https://esm.sh/rehype-parse')
const {default: remarkStringify} = await import('https://esm.sh/remark-stringify')
const {default: rehypeRemark} = await import('https://esm.sh/rehype-remark')
const highlightColor = "rgba(255,255,0,.3)"
let $wrapper, $buttonHighlight;
function buildUI(){
$wrapper = document.querySelector(".pudymody-highlighter")
if( $wrapper !== null ){
return;
}
$wrapper = document.createElement("div");
$wrapper.className = "pudymody-highlighter"
$buttonHighlight = document.createElement("button");
$buttonHighlight.style = `
all: unset;
padding: 0.5rem;
background: #000;
color: #fff;
border-radius: 50%;
cursor: pointer;
display:block;
user-select:none;
position: fixed;
bottom: 0.5rem;
left: 0.5rem;
`;
$buttonHighlight.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:1rem;height:1rem;display:block;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
</svg>`
$buttonHighlight.addEventListener("click", highlightText);
$wrapper.appendChild($buttonHighlight);
$buttonShare = document.createElement("button");
$buttonShare.style = `
all: unset;
padding: 0.5rem;
background: #000;
color: #fff;
border-radius: 50%;
cursor: pointer;
display:block;
user-select:none;
position: fixed;
bottom: 0.5rem;
left: 3rem;
`;
$buttonShare.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:1rem;height:1rem;display:block;">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
</svg>`
$buttonShare.addEventListener("click", share);
$wrapper.appendChild($buttonShare);
document.body.appendChild($wrapper);
}
let selections = [];
function highlightText(){
const currentSelection = window.getSelection();
const totalSelections = currentSelection.rangeCount;
for(let i = 0; i < totalSelections; i++){
const range = currentSelection.getRangeAt(i);
selections.push(
range.cloneContents()
);
[...range.getClientRects()]
.map( e => {
const a = document.createElement("mark");
a.style.position = "absolute";
a.style.pointerEvents = "none";
a.style.background = highlightColor;
a.style.left= (e.left+document.documentElement.scrollLeft)+"px";
a.style.top = (e.top+document.documentElement.scrollTop)+"px";
a.style.height = e.height+"px";
a.style.width = e.width+"px";
return a;
})
.forEach(d => { $wrapper.appendChild(d); });
}
}
const processor = unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkStringify);
function getFileName(){
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = (date.getDate()).toString().padStart(2, "0");
const hour = (date.getHours()).toString().padStart(2, "0");
const minutes = (date.getMinutes()).toString().padStart(2, "0");
return `${year}-${month}-${day}_${hour}-${minutes}.md`
}
function share(){
const fileName = getFileName();
const htmlBody = selections
.flatMap( e => [...e.childNodes] )
.map( e => e.nodeType === e.TEXT_NODE ? `<p>${e.textContent}</p>` : e.outerHTML)
.join("");
const markdownTxt = processor.processSync(htmlBody)
.value
.split("\n\n")
.map( l => `> ${l}`)
markdownTxt.push(`From [${document.title}](${location.toString()})`)
markdownTxt.unshift(`---
layout: "post"
date: ${formatISO(new Date())}
---`);
fileContent = markdownTxt.join("\n\n");
window.open(`https://github.com/pudymody/pudymody.github.io/new/master/content/stream?filename=${encodeURIComponent(fileName)}&value=${encodeURIComponent(fileContent)}`);
}
buildUI();
highlightText();
})(window, document)
Leave your comment on the github issue, sending me an email or DMing me on twitter