-
Notifications
You must be signed in to change notification settings - Fork 324
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linkify): PoC linkification of plaintext IPFS paths
It was not possible to reuse code introduced in #39 (legacy SDK) Requires performance tweaks, but for now closes #202
- Loading branch information
Showing
7 changed files
with
205 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
'use strict' | ||
/* eslint-env browser, webextensions */ | ||
|
||
/* | ||
* This content script is responsible for performing the logic of replacing | ||
* plain text with IPFS addresses with clickable links. | ||
* Loosely based on https://github.com/mdn/webextensions-examples/blob/master/emoji-substitution/substitute.js | ||
* Note that this is a quick&dirty PoC and may slow down browsing experience. | ||
* TODO: measure & improve performance | ||
*/ | ||
|
||
;(function (alreadyLinkified) { | ||
if (alreadyLinkified) { | ||
return | ||
} | ||
|
||
// linkify lock | ||
window.ipfsLinkifiedDOM = true | ||
|
||
const urlRE = /\s+(?:\/ip(f|n)s\/|fs:|ipns:|ipfs:)[^\s+"<>]+/g | ||
|
||
// tags we will scan looking for un-hyperlinked IPFS addresses | ||
const allowedParents = [ | ||
'abbr', 'acronym', 'address', 'applet', 'b', 'bdo', 'big', 'blockquote', 'body', | ||
'caption', 'center', 'cite', 'code', 'dd', 'del', 'div', 'dfn', 'dt', 'em', | ||
'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe', | ||
'ins', 'kdb', 'li', 'object', 'pre', 'p', 'q', 'samp', 'small', 'span', 'strike', | ||
's', 'strong', 'sub', 'sup', 'td', 'th', 'tt', 'u', 'var' | ||
] | ||
|
||
const textNodeXpath = '//text()[(parent::' + allowedParents.join(' or parent::') + ') and ' + | ||
"(contains(., 'ipfs') or contains(., 'ipns')) ]" | ||
|
||
linkifyContainer(document.body) | ||
|
||
// body.appendChild(document.createTextNode('fooo /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w bar')) | ||
new MutationObserver(function (mutations) { | ||
for (let mutation of mutations) { | ||
if (mutation.type === 'childList') { | ||
for (let addedNode of mutation.addedNodes) { | ||
if (addedNode.nodeType === Node.TEXT_NODE) { | ||
linkifyTextNode(addedNode) | ||
} else { | ||
linkifyContainer(addedNode) | ||
} | ||
} | ||
} | ||
if (mutation.type === 'characterData') { | ||
linkifyTextNode(mutation.target) | ||
} | ||
} | ||
}).observe(document.body, { | ||
characterData: true, | ||
childList: true, | ||
subtree: true | ||
}) | ||
|
||
function linkifyContainer (container) { | ||
if (!container.nodeType) { | ||
return | ||
} | ||
if (container.className && container.className.match(/\blinkifiedIpfsAddress\b/)) { | ||
// prevent infinite recursion | ||
return | ||
} | ||
const xpathResult = document.evaluate(textNodeXpath, container, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null) | ||
let i = 0 | ||
function continuation () { | ||
let node = null | ||
let counter = 0 | ||
while ((node = xpathResult.snapshotItem(i++))) { | ||
const parent = node.parentNode | ||
if (!parent) continue | ||
// Skip styled <pre> -- often highlighted by script. | ||
if (parent.tagName === 'PRE' && parent.className) continue | ||
// Skip forms, textareas | ||
if (parent.isContentEditable) continue | ||
linkifyTextNode(node) | ||
if (++counter > 50) { | ||
return setTimeout(continuation, 0) | ||
} | ||
} | ||
} | ||
setTimeout(continuation, 0) | ||
} | ||
|
||
function normalizeHref (href) { | ||
console.log(href) | ||
// convert various variants to regular URL at the public gateway | ||
if (href.startsWith('ipfs:')) { | ||
href = href.replace('ipfs:', '/ipfs/') | ||
} | ||
if (href.startsWith('ipns:')) { | ||
href = href.replace('ipns:', '/ipns/') | ||
} | ||
if (href.startsWith('fs:')) { | ||
href = href.replace('fs:', '') | ||
} | ||
href = 'https://ipfs.io/' + href // for now just point to public gw, we will switch to custom protocol when https://github.com/lidel/ipfs-firefox-addon/issues/164 is closed | ||
href = href.replace(/([^:]\/)\/+/g, '$1') // remove redundant slashes | ||
return href | ||
} | ||
|
||
function linkifyTextNode (node) { | ||
let link | ||
let match | ||
const txt = node.textContent | ||
let span = null | ||
let point = 0 | ||
while ((match = urlRE.exec(txt))) { | ||
if (span == null) { | ||
// Create a span to hold the new text with links in it. | ||
span = document.createElement('span') | ||
span.className = 'linkifiedIpfsAddress' | ||
} | ||
// get the link without trailing dots and commas | ||
link = match[0].replace(/[.,]*$/, '') | ||
const replaceLength = link.length | ||
// put in text up to the link | ||
span.appendChild(document.createTextNode(txt.substring(point, match.index))) | ||
// create a link and put it in the span | ||
const a = document.createElement('a') | ||
a.className = 'linkifiedIpfsAddress' | ||
a.appendChild(document.createTextNode(link)) | ||
a.setAttribute('href', normalizeHref(link.trim())) | ||
span.appendChild(a) | ||
// track insertion point | ||
point = match.index + replaceLength | ||
} | ||
if (span) { | ||
// take the text after the last link | ||
span.appendChild(document.createTextNode(txt.substring(point, txt.length))) | ||
// replace the original text with the new span | ||
try { | ||
node.parentNode.replaceChild(span, node) | ||
} catch (e) { | ||
console.error(e) | ||
console.log(node) | ||
} | ||
} | ||
} | ||
}(window.ipfsLinkifiedDOM)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
'use strict' | ||
/* eslint-env browser, webextensions */ | ||
|
||
const optionDefaults = Object.freeze({ // eslint-disable-line no-unused-vars | ||
publicGateways: 'ipfs.io gateway.ipfs.io ipfs.pics global.upload', | ||
useCustomGateway: true, | ||
automaticMode: true, | ||
linkify: false, | ||
dnslink: false, | ||
customGatewayUrl: 'http://127.0.0.1:8080', | ||
ipfsApiUrl: 'http://127.0.0.1:5001', | ||
ipfsApiPollMs: 3000 | ||
// TODO: | ||
// defaultToFsProtocol | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!doctype html> | ||
<!-- open as resource://ipfs-firefox-addon-at-lidel-dot-org/data/test.html --> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
|
||
<body> | ||
<p id='plain-links'> | ||
ipfs:/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w | ||
<br> ipns://ipfs.git.sexy | ||
<br> fs:/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w | ||
<br> /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w | ||
<br> | ||
</p> | ||
<a href="fs:/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w#1">abs link</a> | ||
<a href="/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w#2" id="relative-ipfs-path">relative link</a> | ||
<a href="http://gateway.ipfs.io/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w#3">gateway link</a> | ||
</body> | ||
|
||
</html> |