Skip to content

Commit

Permalink
feat(linkify): PoC linkification of plaintext IPFS paths
Browse files Browse the repository at this point in the history
It was not possible to reuse code introduced in #39 (legacy SDK)

Requires performance tweaks, but for now closes #202
  • Loading branch information
lidel committed Feb 12, 2017
1 parent afd1081 commit b8f6517
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 21 deletions.
1 change: 1 addition & 0 deletions add-on/src/background/background.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<script src="../lib/npm/is-ipfs.min.js"></script>
<script src="../lib/npm/ipfs-api.min.js"></script>
<script src="../lib/npm/lru.js"></script>
<script src="../lib/option-defaults.js"></script>
<script src="../lib/common.js"></script>
<script src="background.js"></script>
40 changes: 22 additions & 18 deletions add-on/src/lib/common.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'
/* eslint-env browser, webextensions */
/* global optionDefaults */

// INIT
// ===================================================================
Expand Down Expand Up @@ -35,6 +36,7 @@ function initStates (options) {
state.gwURLString = options.customGatewayUrl
state.gwURL = new URL(state.gwURLString)
state.automaticMode = options.automaticMode
state.linkify = options.linkify
state.dnslink = options.dnslink
state.dnslinkCache = /* global LRUMap */ new LRUMap(1000)
getSwarmPeerCount()
Expand Down Expand Up @@ -278,11 +280,24 @@ function updateContextMenus () {
// -------------------------------------------------------------------

function onUpdatedTab (tabId, changeInfo, tab) {
const ipfsContext = window.IsIpfs.url(tab.url)
if (ipfsContext) {
browser.pageAction.show(tab.id)
} else {
browser.pageAction.hide(tab.id)
if (tab && tab.url) {
const ipfsContext = window.IsIpfs.url(tab.url)
if (ipfsContext) {
browser.pageAction.show(tab.id)
} else {
browser.pageAction.hide(tab.id)
}
if (state.linkify && changeInfo.status === 'complete') {
console.log(`Running linkfyDOM for ${tab.url}`)
browser.tabs.executeScript(tabId, {
file: '/src/lib/linkifyDOM.js',
matchAboutBlank: false,
allFrames: true
})
.catch(error => {
console.error(`Unable to linkify DOM at '${tab.url}' due to ${error}`)
})
}
}
}

Expand Down Expand Up @@ -334,19 +349,6 @@ function setBrowserActionBadge (text, color, icon) {
// OPTIONS
// ===================================================================

const optionDefaults = Object.freeze({
publicGateways: 'ipfs.io gateway.ipfs.io ipfs.pics global.upload',
useCustomGateway: true,
automaticMode: true,
dnslink: false,
customGatewayUrl: 'http://127.0.0.1:8080',
ipfsApiUrl: 'http://127.0.0.1:5001',
ipfsApiPollMs: 3000
// TODO:
// linkify
// defaultToFsProtocol
})

function updateAutomaticModeRedirectState () {
// enable/disable gw redirect based on API status and available peer count
if (state.automaticMode) {
Expand Down Expand Up @@ -405,6 +407,8 @@ function onStorageChange (changes, area) { // eslint-disable-line no-unused-vars
} else if (key === 'useCustomGateway') {
state.redirect = change.newValue
browser.alarms.create(ipfsRedirectUpdateAlarm, {})
} else if (key === 'linkify') {
state.linkify = change.newValue
} else if (key === 'automaticMode') {
state.automaticMode = change.newValue
} else if (key === 'dnslink') {
Expand Down
142 changes: 142 additions & 0 deletions add-on/src/lib/linkifyDOM.js
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))
15 changes: 15 additions & 0 deletions add-on/src/lib/option-defaults.js
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
})
7 changes: 4 additions & 3 deletions add-on/src/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@

<form>
<fieldset style="line-height: 2em; font-family: sans-serif;">
<legend>Experimental</legend>
<legend>Experiments <small>(these PoC options may degrade browser performance)</small></legend>

<p><label>Enable <code>dnslink</code> lookups for every website <input type="checkbox" id="dnslink" /> <small>(experimental, degrades browser performance)</small> </label></p>
<p><label>Enable <code>dnslink</code> lookups for every website <input type="checkbox" id="dnslink" /></label></p>
<p><label>Make plaintext IPFS links clickable <input type="checkbox" id="linkify" /></label></p>

</fieldset>
</form>

<script src="../lib/common.js"></script>
<script src="../lib/option-defaults.js"></script>
<script src="../lib/data-i18n.js"></script>
<script src="options.js"></script>

Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module.exports = function (config) {
'test/unit/*.shim.js',
'add-on/src/lib/npm/is-ipfs.min.js',
'add-on/src/lib/npm/ipfs-api.min.js',
'add-on/src/lib/npm/lru.js',
'add-on/src/lib/*.js',
'add-on/src/background/*.js',
'test/unit/*.test.js'
Expand Down
20 changes: 20 additions & 0 deletions test/data/linkify-demo.html
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>

0 comments on commit b8f6517

Please sign in to comment.