Merge branch 'Koushien-completions'

Currently doesn't support "tab" and has lots of dead code. Only
implemented for tabs right now.
This commit is contained in:
Oliver Blanthorn 2017-11-21 19:13:48 +00:00
commit 0aa14bb754
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
12 changed files with 724 additions and 102 deletions

5
package-lock.json generated
View file

@ -3053,6 +3053,11 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fuse.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.0.tgz",
"integrity": "sha1-8ESOgGmFW/Kj5oPNwdMg5+KgfvQ="
},
"fx-runner": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.0.7.tgz",

View file

@ -2,7 +2,9 @@
"name": "tridactyl",
"version": "0.1.0",
"description": "Vimperator/Pentadactyl successor",
"dependencies": {},
"dependencies": {
"fuse.js": "^3.2.0"
},
"devDependencies": {
"@types/jest": "^21.1.4",
"@types/node": "^8.0.46",

View file

@ -21,5 +21,19 @@ export namespace onLine {
}
}
Messaging.addListener("commandline_background", Messaging.attributeCaller({recvExStr}))
/** Helpers for completions */
async function currentWindowTabs(): Promise<browser.tabs.Tab[]> {
return await browser.tabs.query({currentWindow:true})
}
async function allWindowTabs(): Promise<browser.tabs.Tab[]> {
let allTabs: browser.tabs.Tab[] = []
for (const window of await browser.windows.getAll()) {
const tabs = await browser.tabs.query({windowId:window.id})
allTabs = allTabs.concat(tabs)
}
return allTabs
}
Messaging.addListener("commandline_background", Messaging.attributeCaller({currentWindowTabs, recvExStr}))
}

View file

@ -1,21 +1,32 @@
/** Script used in the commandline iframe. Communicates with background. */
import "./lib/html-tagged-template"
import * as Completions from './completions'
import * as Messaging from './messaging'
import * as SELF from './commandline_frame'
import './number.clamp'
import state from './state'
let completionsrc: Completions.CompletionSource = undefined
let completions = window.document.getElementById("completions") as HTMLElement
let clInput = window.document.getElementById("tridactyl-input") as HTMLInputElement
/* This is to handle Escape key which, while the cmdline is focused,
* ends up firing both keydown and input listeners. In the worst case
* hides the cmdline, shows and refocuses it and replaces its text
* which could be the prefix to generate a completion.
* tl;dr TODO: delete this and better resolve race condition
*/
let isVisible = false
function resizeArea() { if (isVisible) sendExstr("showcmdline") }
export let focus = () => clInput.focus()
async function sendExstr(exstr) {
Messaging.message("commandline_background", "recvExStr", [exstr])
}
/* Process the commandline on enter. */
clInput.addEventListener("keydown", function (keyevent) {
switch (keyevent.key) {
@ -63,6 +74,27 @@ clInput.addEventListener("keydown", function (keyevent) {
}
})
clInput.addEventListener("input", async () => {
// TODO: Handle this in parser
if (clInput.value.startsWith("buffer ") || clInput.value.startsWith("tabclose ") ||
clInput.value.startsWith("tabmove ")) {
const tabs: browser.tabs.Tab[] = await Messaging.message("commandline_background", "currentWindowTabs")
completionsrc = Completions.BufferCompletionSource.fromTabs(tabs)
completionsrc = await completionsrc.filter(clInput.value)
completions.innerHTML = ""
completions.appendChild(completionsrc.node)
resizeArea()
}
else if (clInput.value.startsWith("bufferall ")) {
// TODO
}
else if (completionsrc) {
completionsrc = undefined
completions.innerHTML = ""
resizeArea()
}
})
let cmdline_history_position = 0
let cmdline_history_current = ""
@ -71,9 +103,11 @@ function hide_and_clear(){
* keydown event, presumably due to Firefox's internal handler for
* Escape. So clear clInput just after :)
*/
completionsrc = undefined
completions.innerHTML = ""
setTimeout(()=>{clInput.value = ""}, 0)
sendExstr("hidecmdline")
isVisible = false
}
function tabcomplete(){
@ -86,7 +120,7 @@ function tabcomplete(){
function history(n){
completions.innerHTML = ""
if (cmdline_history_position == 0){
cmdline_history_current = clInput.value
cmdline_history_current = clInput.value
}
let wrapped_ind = state.cmdHistory.length + n - cmdline_history_position
wrapped_ind = wrapped_ind.clamp(0, state.cmdHistory.length)
@ -110,7 +144,7 @@ function process() {
state.cmdHistory = state.cmdHistory.concat([clInput.value])
}
console.log(state.cmdHistory)
completionsrc = undefined
completions.innerHTML = ""
clInput.value = ""
cmdline_history_position = 0
@ -123,10 +157,8 @@ export function fillcmdline(newcommand?: string, trailspace = true){
}
// Focus is lost for some reason.
focus()
}
export function changecompletions(newcompletions: string) {
completions.innerHTML = newcompletions
isVisible = true
clInput.dispatchEvent(new Event('input')) // dirty hack for completions
}
function applyWithTmpTextArea(fn) {

188
src/completions.ts Normal file
View file

@ -0,0 +1,188 @@
/*
Have an array of all completion sources. Completion sources display nothing if the filter doesn't match for them.
On each input event, call updateCompletions on the array. That will mutate the array and update the display as required.
How to handle cached e.g. buffer information going out of date?
*/
import * as Fuse from 'fuse.js'
import {enumerate} from './itertools'
import {toNumber} from './convert'
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
// {{{ INTERFACES
interface CompletionOption {
// What to fill into cmdline
value: string
// Highlight and blur element,
blur(): void
focus(): void
}
export abstract class CompletionSource {
private obsolete = false
readonly options = new Array<CompletionOption>()
public node: HTMLElement
// Called by updateCompletions on the child that succeeds its parent
abstract activate(): void
// this.node now belongs to you, update it or something :)
// Example: Mutate node or call replaceChild on its parent
abstract async filter(exstr): Promise<CompletionSource>
// <Do some async work that doesn't mutate any non-local vars>
// Make a new CompletionOptions and return it
}
// }}}
// {{{ IMPLEMENTATIONS
class BufferCompletionOption implements CompletionOption {
// keep a reference to our markup so we can highlight it on focus().
html: HTMLElement
// For fuzzy matching
matchStrings: string[] = []
constructor(public value: string, tab: browser.tabs.Tab, isAlternative = false) {
// Two character buffer properties prefix
let pre = ""
if (tab.active) pre += "%"
else if (isAlternative) pre += "#"
if (tab.pinned) { pre += "@" }
this.matchStrings.push(pre) // before pad so we don't match whitespace
pre = pre.padEnd(2)
this.matchStrings.push(String(tab.index + 1), tab.title, tab.url)
const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<div class="BufferCompletionOption option">
<span>${pre}</span>
<img src=${favIconUrl} />
<span>${tab.index + 1}: ${tab.title}</span>
<a class="url" href=${tab.url}>${tab.url}</a>
</div>`
}
blur() { this.html.classList.remove("focused") }
focus() { this.html.classList.add("focused"); this.show() }
hide() { this.html.classList.add("hidden"); this.blur() }
show() { this.html.classList.remove("hidden") }
}
export class BufferCompletionSource extends CompletionSource {
private fuse: Fuse
public prefixes = [ "buffer " ]
constructor(
readonly options: BufferCompletionOption[],
public node: HTMLElement,
) {
super()
const fuseOptions = {
keys: ["matchStrings"],
shouldSort: true,
id: "index",
}
// Can't sort the real options array because Fuse loses class information.
const searchThis = options.map((elem, index) => {return {index, matchStrings: elem.matchStrings}})
this.fuse = new Fuse(searchThis, fuseOptions)
}
static fromTabs(tabs: browser.tabs.Tab[]) {
const node = html`<div class="BufferCompletionSource">
<div class="sectionHeader">Buffers</div>`
// Get alternative tab, defined as last accessed tab.
const alt = tabs.sort((a, b) => { return a.lastAccessed < b.lastAccessed ? 1 : -1 })[1]
tabs.sort((a, b) => { return a.index < b.index ? -1 : 1 })
const options: BufferCompletionOption[] = []
for (const tab of tabs) {
options.push(new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt)
)
}
for (const option of options) {
node.appendChild(option.html)
}
return new BufferCompletionSource(options, node)
}
activate() {
// TODO... this bit of the interface isn't super clear to me yet.
}
async filter(exstr: string) {
// Remove the `${prefix} ` bit.
const query = exstr.slice(exstr.indexOf(' ') + 1)
if (query) {
let matches = this.fuse.search(query).map(toNumber) as number[]
for (const [index, option] of enumerate(this.options)) {
if (! matches.includes(index)) option.hide()
else option.show()
}
if (matches.length) this.options[matches[0]].focus()
} else {
for (const option of this.options) {
option.show()
}
}
return this
}
}
// }}}
// {{{ UNUSED: MANAGING ASYNC CHANGES
/* If first to modify completions, update it. */
async function commitIfCurrent(epochref: any, asyncFunc: Function, commitFunc: Function, ...args: any[]): Promise<any> {
// I *think* sync stuff in here is guaranteed to happen immediately after
// being called, up to the first await, despite this being an async
// function. But I don't know. Should check.
const epoch = epochref
const res = await asyncFunc(...args)
if (epoch === epochref) return commitFunc(res)
else console.error(new Error("Update failed: epoch out of date!"))
}
/* Indicate changes to completions we would like. */
function updateCompletions(filter: string, sources: CompletionSource[]) {
for (let [index, source] of enumerate(sources)) {
// Tell each compOpt to filter, and if they finish fast enough they:
// 0. Leave a note for any siblings that they got here first
// 1. Take over their parent's slot in compOpts
// 2. Update their display
commitIfCurrent(
source.obsolete, // Flag/epoch
source.filter, // asyncFunc
(childSource) => { // commitFunc
source.obsolete = true
sources[index] = childSource
childSource.activate()
},
filter // argument to asyncFunc
)
}
}
// }}}

View file

@ -3,6 +3,7 @@
// Be careful: typescript elides imports that appear not to be used if they're
// assigned to a name. If you want an import just for its side effects, make
// sure you import it like this:
import "./lib/html-tagged-template"
/* import "./keydown_content" */
/* import "./commandline_content" */
/* import "./excmds_content" */

View file

@ -580,17 +580,14 @@ export async function clipboard(excmd: "open"|"yank"|"tabopen" = "open", ...toYa
// {{{ Buffer/completion stuff
// TODO: Move autocompletions out of excmds.
/** @hidden */
//#background_helper
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
/** Soon to be deprecated way of showing buffer completions */
/** Ported from Vimperator. */
//#background
export async function openbuffer() {
export async function tabs() {
fillcmdline("buffer")
messageActiveTab("commandline_frame", "changecompletions", [await l(listTabs())])
showcmdline()
}
//#background
export async function buffers() {
tabs()
}
/** Change active tab */
@ -618,66 +615,10 @@ export async function buffer(n?: number | string) {
}
}
/** List of tabs in window and the last active tab. */
/** @hidden */
//#background_helper
async function getTabs() {
const tabs = await browser.tabs.query({currentWindow: true})
const lastActive = tabs.sort((a, b) => {
return a.lastAccessed < b.lastAccessed ? 1 : -1
})[1]
tabs.sort((a, b) => {
return a.index < b.index ? -1 : 1
})
console.log(tabs)
return [tabs, lastActive]
}
/** innerHTML for a single Tab's representation in autocompletion */
/** @hidden */
//#background_helper
function formatTab(tab: browser.tabs.Tab, prev?: boolean) {
// This, like all this completion logic, needs to move.
const tabline = window.document.createElement('div')
tabline.className = "tabline"
const prefix = window.document.createElement('span')
if (tab.active) prefix.textContent += "%"
else if (prev) prefix.textContent += "#"
if (tab.pinned) prefix.textContent += "@"
prefix.textContent = prefix.textContent.padEnd(2)
tabline.appendChild(prefix)
// TODO: Dynamically set favicon dimensions. Should be able to use em.
const favicon = window.document.createElement('img')
favicon.src = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
tabline.appendChild(favicon)
const titlespan = window.document.createElement('span')
titlespan.textContent=`${tab.index + 1}: ${tab.title}`
tabline.appendChild(titlespan)
const url = window.document.createElement('a')
url.className = 'url'
url.href = tab.url
url.text = tab.url
url.target = '_blank'
tabline.appendChild(url)
console.log(tabline)
return tabline.outerHTML
}
/** innerHTML for tab autocompletion div */
/** @hidden */
//#background_helper
async function listTabs() {
let buffers: string = "",
[tabs, lastActive] = await getTabs()
for (let tab of tabs as Array<browser.tabs.Tab>) {
buffers += tab === lastActive ? formatTab(tab, true) : formatTab(tab)
}
return buffers
/** Set tab with index of n belonging to window with id of m to active */
//#background
export async function bufferall(m?: number, n?: number) {
// TODO
}
// }}}
@ -776,7 +717,6 @@ export function hint(option?: "-b") {
else hinting.hintPageSimple()
}
// }}}
// {{{ GOBBLE mode

View file

@ -0,0 +1,408 @@
(function(window) {
"use strict";
// test for es6 support of needed functionality
try {
// spread operator and template strings support
(function testSpreadOpAndTemplate() {
const tag = function tag(strings, ...values) {return;};
tag`test`;
})();
// template tag and Array.from support
if (!('content' in document.createElement('template') && 'from' in Array)) {
throw new Error();
}
}
catch (e) {
// missing support;
console.log('Your browser does not support the needed functionality to use the html tagged template');
return;
}
if (typeof window.html === 'undefined') {
// --------------------------------------------------
// constants
// --------------------------------------------------
const SUBSTITUTION_INDEX = 'substitutionindex:'; // tag names are always all lowercase
const SUBSTITUTION_REGEX = new RegExp(SUBSTITUTION_INDEX + '([0-9]+):', 'g');
// rejection string is used to replace xss attacks that cannot be escaped either
// because the escaped string is still executable
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
const REJECTION_STRING = 'zXssPreventedz';
// which characters should be encoded in which contexts
const ENCODINGS = {
attribute: {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
},
uri: {
'&': '&amp;'
}
};
// which attributes are DOM Level 0 events
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
const DOM_EVENTS = ["onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover", "onmousemove", "onmouseout", "ondragstart", "ondrag", "ondragenter", "ondragleave", "ondragover", "ondrop", "ondragend", "onkeydown", "onkeypress", "onkeyup", "onload", "onunload", "onabort", "onerror", "onresize", "onscroll", "onselect", "onchange", "onsubmit", "onreset", "onfocus", "onblur", "onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerover", "onpointerout", "onpointerenter", "onpointerleave", "ongotpointercapture", "onlostpointercapture", "oncut", "oncopy", "onpaste", "onbeforecut", "onbeforecopy", "onbeforepaste", "onafterupdate", "onbeforeupdate", "oncellchange", "ondataavailable", "ondatasetchanged", "ondatasetcomplete", "onerrorupdate", "onrowenter", "onrowexit", "onrowsdelete", "onrowinserted", "oncontextmenu", "ondrag", "ondragstart", "ondragenter", "ondragover", "ondragleave", "ondragend", "ondrop", "onselectstart", "help", "onbeforeunload", "onstop", "beforeeditfocus", "onstart", "onfinish", "onbounce", "onbeforeprint", "onafterprint", "onpropertychange", "onfilterchange", "onreadystatechange", "onlosecapture", "DOMMouseScroll", "ondragdrop", "ondragenter", "ondragexit", "ondraggesture", "ondragover", "onclose", "oncommand", "oninput", "DOMMenuItemActive", "DOMMenuItemInactive", "oncontextmenu", "onoverflow", "onoverflowchanged", "onunderflow", "onpopuphidden", "onpopuphiding", "onpopupshowing", "onpopupshown", "onbroadcast", "oncommandupdate"];
// which attributes take URIs
// taken from https://www.w3.org/TR/html4/index/attributes.html
const URI_ATTRIBUTES = ["action", "background", "cite", "classid", "codebase", "data", "href", "longdesc", "profile", "src", "usemap"];
const ENCODINGS_REGEX = {
attribute: new RegExp('[' + Object.keys(ENCODINGS.attribute).join('') + ']', 'g'),
uri: new RegExp('[' + Object.keys(ENCODINGS.uri).join('') + ']', 'g')
};
// find all attributes after the first whitespace (which would follow the tag
// name. Only used when the DOM has been clobbered to still parse attributes
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g;
// test if a javascript substitution is wrapped with quotes
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/;
// allow custom attribute names that start or end with url or ui to do uri escaping
// @see https://developers.google.com/closure/templates/docs/security#in_urls
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i;
// --------------------------------------------------
// private functions
// --------------------------------------------------
/**
* Escape HTML entities in an attribute.
* @private
*
* @param {string} str - String to escape.
*
* @returns {string}
*/
function encodeAttributeHTMLEntities(str) {
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
return ENCODINGS.attribute[match];
});
}
/**
* Escape entities in a URI.
* @private
*
* @param {string} str - URI to escape.
*
* @returns {string}
*/
function encodeURIEntities(str) {
return str.replace(ENCODINGS_REGEX.uri, function(match) {
return ENCODINGS.uri[match];
});
}
// --------------------------------------------------
// html tagged template function
// --------------------------------------------------
/**
* Safely convert a DOM string into DOM nodes using by using E4H and contextual
* auto-escaping techniques to prevent xss attacks.
*
* @param {string[]} strings - Safe string literals.
* @param {*} values - Unsafe substitution expressions.
*
* @returns {HTMLElement|DocumentFragment}
*/
window.html = function(strings, ...values) {
// break early if called with empty content
if (!strings[0] && values.length === 0) {
return;
}
/**
* Replace a string with substitution placeholders with its substitution values.
* @private
*
* @param {string} match - Matched substitution placeholder.
* @param {string} index - Substitution placeholder index.
*/
function replaceSubstitution(match, index) {
return values[parseInt(index, 10)];
}
// insert placeholders into the generated string so we can run it through the
// HTML parser without any malicious content.
// (this particular placeholder will even work when used to create a DOM element)
let str = strings[0];
for (let i = 0; i < values.length; i++) {
str += SUBSTITUTION_INDEX + i + ':' + strings[i+1];
}
// template tags allow any HTML (even <tr> elements out of context)
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
let template = document.createElement('template');
template.innerHTML = str;
// find all substitution values and safely encode them using DOM APIs and
// contextual auto-escaping
let walker = document.createNodeIterator(template.content, NodeFilter.SHOW_ALL);
let node;
while (node = walker.nextNode()) {
let tag = null;
let attributesToRemove = [];
// --------------------------------------------------
// node name substitution
// --------------------------------------------------
let nodeName = node.nodeName.toLowerCase();
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
nodeName = nodeName.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// createElement() should not need to be escaped to prevent XSS?
// this will throw an error if the tag name is invalid (e.g. xss tried
// to escape out of the tag using '><script>alert(1337)</script><')
// instead of replacing the tag name we'll just let the error be thrown
tag = document.createElement(nodeName);
// mark that this node needs to be cleaned up later with the newly
// created node
node._replacedWith = tag;
// use insertBefore() instead of replaceChild() so that the node Iterator
// doesn't think the new tag should be the next node
node.parentNode.insertBefore(tag, node);
}
// special case for script tags:
// using innerHTML with a string that contains a script tag causes the script
// tag to not be executed when added to the DOM. We'll need to create a script
// tag and append its contents which will make it execute correctly.
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
else if (node.nodeName === 'SCRIPT') {
let script = document.createElement('script');
tag = script;
node._replacedWith = script;
node.parentNode.insertBefore(script, node);
}
// --------------------------------------------------
// attribute substitution
// --------------------------------------------------
let attributes;
if (node.attributes) {
// if the attributes property is not of type NamedNodeMap then the DOM
// has been clobbered. E.g. <form><input name="attributes"></form>.
// We'll manually build up an array of objects that mimic the Attr
// object so the loop will still work as expected.
if ( !(node.attributes instanceof NamedNodeMap) ) {
// first clone the node so we can isolate it from any children
let temp = node.cloneNode();
// parse the node string for all attributes
let attributeMatches = temp.outerHTML.match(ATTRIBUTE_PARSER_REGEX);
// get all attribute names and their value
attributes = [];
for (let i = 0; i < attributeMatches.length; i++) {
let attributeName = attributeMatches[i].trim().split('=')[0];
let attributeValue = node.getAttribute(attributeName);
attributes.push({
name: attributeName,
value: attributeValue
});
}
}
else {
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
// push the attribute to the end when using setAttribute(). We'll have
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
attributes = Array.from(node.attributes);
}
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
let name = attribute.name;
let value = attribute.value;
let hasSubstitution = false;
// name has substitution
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
name = name.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// ensure substitution was with a non-empty string
if (name && typeof name === 'string') {
hasSubstitution = true;
}
// remove old attribute
attributesToRemove.push(attribute.name);
}
// value has substitution - only check if name exists (only happens
// when name is a substitution with an empty value)
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
hasSubstitution = true;
// if an uri attribute has been rejected
let isRejected = false;
value = value.replace(SUBSTITUTION_REGEX, function(match, index, offset) {
if (isRejected) {
return '';
}
let substitutionValue = values[parseInt(index, 10)];
// contextual auto-escaping:
// if attribute is a DOM Level 0 event then we need to ensure it
// is quoted
if (DOM_EVENTS.indexOf(name) !== -1 &&
typeof substitutionValue === 'string' &&
!WRAPPED_WITH_QUOTES_REGEX.test(substitutionValue) ) {
substitutionValue = '"' + substitutionValue + '"';
}
// contextual auto-escaping:
// if the attribute is a uri attribute then we need to uri encode it and
// remove bad protocols
else if (URI_ATTRIBUTES.indexOf(name) !== -1 ||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)) {
// percent encode if the value is inside of a query parameter
let queryParamIndex = value.indexOf('=');
if (queryParamIndex !== -1 && offset > queryParamIndex) {
substitutionValue = encodeURIComponent(substitutionValue);
}
// entity encode if value is part of the URL
else {
substitutionValue = encodeURI( encodeURIEntities(substitutionValue) );
// only allow the : when used after http or https otherwise reject
// the entire url (will not allow any 'javascript:' or filter
// evasion techniques)
if (offset === 0 && substitutionValue.indexOf(':') !== -1) {
let protocol = substitutionValue.substring(0, 5);
if (protocol.indexOf('http') === -1) {
isRejected = true;
}
}
}
}
// contextual auto-escaping:
// HTML encode attribute value if it is not a URL or URI to prevent
// DOM Level 0 event handlers from executing xss code
else if (typeof substitutionValue === 'string') {
substitutionValue = encodeAttributeHTMLEntities(substitutionValue);
}
return substitutionValue;
});
if (isRejected) {
value = '#' + REJECTION_STRING;
}
}
// add the attribute to the new tag or replace it on the current node
// setAttribute() does not need to be escaped to prevent XSS since it does
// all of that for us
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
if (tag || hasSubstitution) {
let el = (tag || node);
// optional attribute
if (name.substr(-1) === '?') {
el.removeAttribute(name);
if (value === 'true') {
name = name.slice(0, -1);
el.setAttribute(name, '');
}
}
else {
el.setAttribute(name, value);
}
}
}
}
// remove placeholder attributes outside of the attribute loop since it
// will modify the attributes NamedNodeMap indices.
// @see https://github.com/straker/html-tagged-template/issues/13
attributesToRemove.forEach(function(attribute) {
node.removeAttribute(attribute);
});
// append the current node to a replaced parent
let parentNode;
if (node.parentNode && node.parentNode._replacedWith) {
parentNode = node.parentNode;
node.parentNode._replacedWith.appendChild(node);
}
// remove the old node from the DOM
if ((node._replacedWith && node.childNodes.length === 0) ||
(parentNode && parentNode.childNodes.length === 0) ){
(parentNode || node).remove();
}
// --------------------------------------------------
// text content substitution
// --------------------------------------------------
if (node.nodeType === 3 && node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1) {
let nodeValue = node.nodeValue.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// createTextNode() should not need to be escaped to prevent XSS?
let text = document.createTextNode(nodeValue);
// since the parent node has already gone through the iterator, we can use
// replaceChild() here
node.parentNode.replaceChild(text, node);
}
}
// return the documentFragment for multiple nodes
if (template.content.childNodes.length > 1) {
return template.content;
}
return template.content.firstChild;
};
}
})(window);

View file

@ -44,7 +44,8 @@ export const DEFAULTNMAPS = {
"S": "fillcmdline tabopen google",
"M": "gobble 1 quickmark",
"xx": "something",
"b": "openbuffer",
"B": "fillcmdline bufferall",
"b": "fillcmdline buffer",
"ZZ": "qall",
"f": "hint",
"F": "hint -b",

View file

@ -7,7 +7,8 @@ input {
width: 100%;
padding: 0;
font-family: "monospace", "Courier New";
font-size: 10pt;
font-size: 9pt;
line-height: 1.5;
color: black;
border: unset;
/* we currently have a border from the completions */
@ -15,35 +16,59 @@ input {
padding-left: 0.5ex;
}
img {
height: 1em;
width: 1em;
.hidden {
display: none
}
.focused {
background: darkgrey;
color: white;
}
/* COMPLETIONS */
#completions {
--option-height: 1.4em;
background: white;
color: black;
display: inline-block;
font-size: 10pt;
height: auto;
/* line-height: 1.5em; */
font-family: "monospace", "Courier New";
max-height: calc(10 * var(--option-height));
font-family: "monospace";
overflow: hidden;
white-space: pre;
width: 100%;
z-index: 9998;
border-top: 1px solid lightgray;
border-top: 0.5px solid grey;
}
.url, a.url, a.url:link, a.url:visited {
background: white;
color: #1f9947;
#completions img {
display: inline;
vertical-align: middle;
height: 1em;
width: 1em;
}
#completions .sectionHeader {
background: linear-gradient(lightgrey, #ddd, lightgrey);
font-weight: bold;
border-bottom: 0.5px solid grey;
padding-left: 0.5ex;
}
#completions .sectionHeader, #completions .option {
height: --option-height;
line-height: --option-height;
}
/* #completions, but I can't be bothered to write it a bunch of times */
.url, a.url, a.url:link, a.url:visited {
color: #1f9947;
left: 50%;
overflow: hidden;
padding: 0 2px;
margin-left: 10px;
position: absolute;
text-decoration: none;
/* Without this the last URL flows over onto a second line if it's long enough */
white-space: pre;
}
/* Hide the URLs if the screen is small */
@ -58,11 +83,7 @@ a.url:hover {
text-decoration: underline !important;
}
div.tabline span {
.BufferCompletionOption span {
white-space: pre;
margin: 0 .5em;
}
div.tabline img {
display: inline;
vertical-align: middle;
}

8
src/tridactyl.d.ts vendored
View file

@ -16,6 +16,11 @@ interface Window {
scrollByPages(n: number): void
}
// Fix typescript bugs
interface StringConstructor {
toLowerCase(): string;
}
// Web extension types not in web-ext-types yet
declare namespace browser.find {
function find(query, object): any
@ -25,3 +30,6 @@ declare namespace browser.find {
declare namespace browser.tabs {
function setZoom(number): any
}
// html-tagged-template.js
declare function html(strings: TemplateStringsArray, ...values: any[]): HTMLElement

View file

@ -1,12 +1,14 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"noImplicitAny": false,
"noEmitOnError": true,
"outDir": "build/tsc-out",
"sourceMap": true,
"target": "ES2017",
"lib": ["ES2017","dom"],
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types/"]
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types/"],
"alwaysStrict": true
},
"include": [
"./src/**/*"