mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 09:31:41 -05:00
Merge branch 'master' into i236-common-ex-aliases
This commit is contained in:
commit
2441fba8c0
17 changed files with 269 additions and 72 deletions
|
@ -76,7 +76,7 @@ NOTE: key modifiers (eg: control, alt) are not supported yet. See the FAQ below.
|
|||
|
||||
- Why doesn't Tridactyl work on websites with frames?
|
||||
|
||||
It should work on some frames now. See [#122](https://github.com/cmcaine/tridactyl/issues/122).
|
||||
It should work on some frames now. See [#122](https://github.com/cmcaine/tridactyl/issues/122).
|
||||
|
||||
- Can I change proxy via commands?
|
||||
|
||||
|
@ -92,7 +92,7 @@ NOTE: key modifiers (eg: control, alt) are not supported yet. See the FAQ below.
|
|||
|
||||
- Why doesn't Tridactyl work on some pages?
|
||||
|
||||
One possible reason is that the site has a strict content security policy. We can rewrite these to make Tridactyl work, but we do not want to worsen the security of sensitive pages, so it is taking us a little while. See #112.
|
||||
One possible reason is that the site has a strict content security policy. We can rewrite these to make Tridactyl work, but we do not want to worsen the security of sensitive pages, so it is taking us a little while. See [#112](https://github.com/cmcaine/tridactyl/issues/112).
|
||||
|
||||
- How can I know which mode I'm in/have a status line?
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/** Inject an input element into unsuspecting webpages and provide an API for interaction with tridactyl */
|
||||
|
||||
import Logger from './logging'
|
||||
const logger = new Logger('messaging')
|
||||
|
||||
/* TODO:
|
||||
CSS
|
||||
Friendliest-to-webpage way of injecting commandline bar?
|
||||
|
@ -16,14 +19,13 @@ let cmdline_iframe: HTMLIFrameElement = undefined
|
|||
function init(){
|
||||
if (cmdline_iframe === undefined && window.document.body !== null) {
|
||||
try {
|
||||
console.log("INIT")
|
||||
cmdline_iframe = window.document.createElement("iframe")
|
||||
cmdline_iframe.setAttribute("src", browser.extension.getURL("static/commandline.html"))
|
||||
cmdline_iframe.setAttribute("id", "cmdline_iframe")
|
||||
hide()
|
||||
window.document.body.appendChild(cmdline_iframe)
|
||||
} catch (e) {
|
||||
console.error("Couldn't initialise cmdline_iframe!", e)
|
||||
logger.error("Couldn't initialise cmdline_iframe!", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import * as SELF from './commandline_frame'
|
|||
import './number.clamp'
|
||||
import state from './state'
|
||||
import * as Config from './config'
|
||||
import Logger from './logging'
|
||||
const logger = new Logger('cmdline')
|
||||
|
||||
let activeCompletions: Completions.CompletionSource[] = undefined
|
||||
let completionsDiv = window.document.getElementById("completions") as HTMLElement
|
||||
|
@ -161,7 +163,7 @@ clInput.addEventListener("input", () => {
|
|||
}
|
||||
|
||||
// Fire each completion and add a callback to resize area
|
||||
console.log(activeCompletions)
|
||||
logger.debug(activeCompletions)
|
||||
activeCompletions.forEach(comp =>
|
||||
comp.filter(newCmd).then(() => resizeArea())
|
||||
)
|
||||
|
@ -212,9 +214,7 @@ function history(n){
|
|||
|
||||
/* Send the commandline to the background script and await response. */
|
||||
function process() {
|
||||
console.log(clInput.value)
|
||||
const command = getCompletion() || clInput.value
|
||||
console.log(command)
|
||||
|
||||
hide_and_clear()
|
||||
|
||||
|
@ -225,7 +225,6 @@ function process() {
|
|||
) {
|
||||
state.cmdHistory = state.cmdHistory.concat([command])
|
||||
}
|
||||
console.log(state.cmdHistory)
|
||||
cmdline_history_position = 0
|
||||
|
||||
sendExstr(command)
|
||||
|
@ -263,7 +262,7 @@ export function setClipboard(content: string) {
|
|||
scratchpad.select()
|
||||
if (document.execCommand("Copy")) {
|
||||
// // todo: Maybe we can consider to using some logger and show it with status bar in the future
|
||||
console.log('set clipboard:', scratchpad.value)
|
||||
logger.info('set clipboard:', scratchpad.value)
|
||||
} else throw "Failed to copy!"
|
||||
})
|
||||
}
|
||||
|
@ -272,7 +271,6 @@ export function getClipboard() {
|
|||
return applyWithTmpTextArea(scratchpad => {
|
||||
scratchpad.focus()
|
||||
document.execCommand("Paste")
|
||||
console.log('get clipboard', scratchpad.textContent)
|
||||
return scratchpad.textContent
|
||||
})
|
||||
}
|
||||
|
|
|
@ -622,17 +622,21 @@ export class BufferCompletionSource extends CompletionSourceFuse {
|
|||
super.setStateFromScore(scoredOpts, true)
|
||||
}
|
||||
|
||||
/** Score with fuse unless query is an integer or a single # */
|
||||
/** Score with fuse unless query is a single # or looks like a buffer index */
|
||||
scoredOptions(query: string, options = this.options): ScoredOption[] {
|
||||
const args = query.split(/\s+/gu)
|
||||
if (args.length <= 2) {
|
||||
const args = query.trim().split(/\s+/gu)
|
||||
if (args.length === 1) {
|
||||
// if query is an integer n and |n| < options.length
|
||||
if (Number.isInteger(Number(args[0]))) {
|
||||
const index = (Number(args[0]) - 1).mod(options.length)
|
||||
return [{
|
||||
index,
|
||||
option: options[index],
|
||||
score: 0,
|
||||
}]
|
||||
let index = Number(args[0]) - 1
|
||||
if (Math.abs(index) < options.length) {
|
||||
index = index.mod(options.length)
|
||||
return [{
|
||||
index,
|
||||
option: options[index],
|
||||
score: 0,
|
||||
}]
|
||||
}
|
||||
} else if (args[0] === '#') {
|
||||
for (const [index, option] of enumerate(options)) {
|
||||
if (option.isAlternative) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
//
|
||||
// Really, we'd like a way of just letting things use the variables
|
||||
//
|
||||
|
||||
const CONFIGNAME = "userconfig"
|
||||
|
||||
type StorageMap = browser.storage.StorageMap
|
||||
|
@ -147,6 +148,16 @@ const DEFAULTS = o({
|
|||
"ttsrate": 1, // 0.1 to 10
|
||||
"ttspitch": 1, // 0 to 2
|
||||
"vimium-gi": true,
|
||||
|
||||
// Default logging levels - 2 === WARNING
|
||||
"logging": o({
|
||||
"messaging": 2,
|
||||
"cmdline": 2,
|
||||
"controller": 2,
|
||||
"hinting": 2,
|
||||
"state": 2,
|
||||
"excmd": 1,
|
||||
}),
|
||||
})
|
||||
|
||||
// currently only supports 2D or 1D storage
|
||||
|
|
|
@ -3,6 +3,7 @@ import {isTextEditable} from './dom'
|
|||
import {isSimpleKey} from './keyseq'
|
||||
import state from "./state"
|
||||
import {repeat} from './excmds_background'
|
||||
import Logger from "./logging"
|
||||
|
||||
import {parser as exmode_parser} from './parsers/exmode'
|
||||
import {parser as hintmode_parser} from './hinting_background'
|
||||
|
@ -13,6 +14,7 @@ import * as gobblemode from './parsers/gobblemode'
|
|||
import * as inputmode from './parsers/inputmode'
|
||||
|
||||
|
||||
const logger = new Logger('controller')
|
||||
|
||||
/** Accepts keyevents, resolves them to maps, maps to exstrs, executes exstrs */
|
||||
function *ParserController () {
|
||||
|
@ -41,7 +43,7 @@ function *ParserController () {
|
|||
state.mode = "normal"
|
||||
}
|
||||
}
|
||||
console.log(keyevent, state.mode)
|
||||
logger.debug(keyevent, state.mode)
|
||||
|
||||
// Special keys (e.g. Backspace) are not handled properly
|
||||
// yet. So drop them. This also drops all modifier keys.
|
||||
|
@ -61,7 +63,7 @@ function *ParserController () {
|
|||
response = (parsers[state.mode] as any)([keyevent])
|
||||
break
|
||||
}
|
||||
console.debug(keys, response)
|
||||
logger.debug(keys, response)
|
||||
|
||||
if (response.ex_str){
|
||||
ex_str = response.ex_str
|
||||
|
|
|
@ -66,13 +66,9 @@ export async function downloadUrl(url: string, saveAs: boolean) {
|
|||
|
||||
// TODO: at this point, could give feeback using the promise returned
|
||||
// by downloads.download(), needs status bar to show it (#90)
|
||||
downloadPromise.then(id => {
|
||||
//console.log("Downloaded OK: ", urlToSave)
|
||||
},
|
||||
error => {
|
||||
console.log("Failed to download: ", urlToDownload, error)
|
||||
}
|
||||
)
|
||||
// By awaiting the promise, we ensure that if it errors, the error will be
|
||||
// thrown by this function too.
|
||||
await downloadPromise
|
||||
}
|
||||
|
||||
import * as Messaging from "./messaging"
|
||||
|
|
|
@ -95,6 +95,8 @@ import * as CommandLineBackground from './commandline_background'
|
|||
import * as DOM from './dom'
|
||||
|
||||
import * as config from './config'
|
||||
import * as Logging from "./logging"
|
||||
const logger = new Logging.Logger('excmds')
|
||||
|
||||
|
||||
/** @hidden */
|
||||
|
@ -115,18 +117,17 @@ function hasScheme(uri: string) {
|
|||
/** @hidden */
|
||||
function searchURL(provider: string, query: string) {
|
||||
if (provider == "search") provider = config.get("searchengine")
|
||||
let searchurlprovider = config.get("searchurls", provider)
|
||||
if (searchurlprovider !== undefined){
|
||||
const url = new URL(searchurlprovider + encodeURIComponent(query))
|
||||
// URL constructor doesn't convert +s because they're valid literals in
|
||||
// the standard it adheres to. But they are special characters in
|
||||
// x-www-form-urlencoded and e.g. google excepts query parameters in
|
||||
// that format.
|
||||
url.search = url.search.replace(/\+/g, '%2B')
|
||||
return url
|
||||
} else {
|
||||
const searchurlprovider = config.get("searchurls", provider)
|
||||
if (searchurlprovider === undefined){
|
||||
throw new TypeError(`Unknown provider: '${provider}'`)
|
||||
}
|
||||
|
||||
// build search URL: either replace "%s" in URL with query or append query to URL
|
||||
const url = searchurlprovider.includes("%s") ?
|
||||
new URL(searchurlprovider.replace("%s", encodeURIComponent(query))) :
|
||||
new URL(searchurlprovider + encodeURIComponent(query))
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/** If maybeURI doesn't have a schema, affix http:// */
|
||||
|
@ -145,7 +146,6 @@ function forceURI(maybeURI: string): string {
|
|||
const args = maybeURI.split(' ')
|
||||
return searchURL(args[0], args.slice(1).join(' ')).href
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e.name !== 'TypeError') throw e
|
||||
}
|
||||
|
||||
|
@ -172,6 +172,33 @@ function tabSetActive(id: number) {
|
|||
|
||||
// }}}
|
||||
|
||||
// {{{ INTERNAL/DEBUG
|
||||
|
||||
/**
|
||||
* Set the logging level for a given logging module.
|
||||
*
|
||||
* @param logModule the logging module to set the level on
|
||||
* @param level the level to log at: in increasing verbosity, one of
|
||||
* "never", "error", "warning", "info", "debug"
|
||||
*/
|
||||
//#background
|
||||
export function loggingsetlevel(logModule: string, level: string) {
|
||||
const map = {
|
||||
"never": Logging.LEVEL.NEVER,
|
||||
"error": Logging.LEVEL.ERROR,
|
||||
"warning": Logging.LEVEL.WARNING,
|
||||
"info": Logging.LEVEL.INFO,
|
||||
"debug": Logging.LEVEL.DEBUG,
|
||||
}
|
||||
|
||||
let newLevel = map[level.toLowerCase()]
|
||||
|
||||
if (newLevel !== undefined) {
|
||||
config.set("logging", newLevel, logModule)
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ PAGE CONTEXT
|
||||
|
||||
|
@ -270,7 +297,6 @@ export async function reloadhard(n = 1) {
|
|||
//#content
|
||||
export function open(...urlarr: string[]) {
|
||||
let url = urlarr.join(" ")
|
||||
console.log("open url:" + url)
|
||||
window.location.href = forceURI(url)
|
||||
}
|
||||
|
||||
|
@ -284,7 +310,6 @@ export function open(...urlarr: string[]) {
|
|||
//#background
|
||||
export function home(all: "false" | "true" = "false"){
|
||||
let homepages = config.get("homepages")
|
||||
console.log(homepages)
|
||||
if (homepages.length > 0){
|
||||
if (all === "false") open(homepages[homepages.length - 1])
|
||||
else {
|
||||
|
@ -904,7 +929,7 @@ export function repeat(n = 1, ...exstr: string[]) {
|
|||
let cmd = state.last_ex_str
|
||||
if (exstr.length > 0)
|
||||
cmd = exstr.join(" ")
|
||||
console.log("repeating " + cmd + " " + n + " times")
|
||||
logger.debug("repeating " + cmd + " " + n + " times")
|
||||
for (let i = 0; i < n; i++)
|
||||
controller.acceptExCmd(cmd)
|
||||
}
|
||||
|
@ -1177,12 +1202,10 @@ export async function sanitize(...args: string[]) {
|
|||
}
|
||||
since = { "since": (new Date()).getTime() - millis }
|
||||
} else {
|
||||
console.log(":sanitize error: expected time format: ^([0-9])+(m|h|d|w)$, given format:" + args[flagpos+1])
|
||||
return
|
||||
throw new Error(":sanitize error: expected time format: ^([0-9])+(m|h|d|w)$, given format:" + args[flagpos+1])
|
||||
}
|
||||
} else {
|
||||
console.log(":sanitize error: -t given but no following arguments")
|
||||
return
|
||||
throw new Error(":sanitize error: -t given but no following arguments")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1418,10 +1441,10 @@ export async function ttsread(mode: "-t" | "-c", ...args: string[]) {
|
|||
if (args.length > 0) {
|
||||
tssReadFromCss(args[0])
|
||||
} else {
|
||||
console.log("Error: no CSS selector supplied")
|
||||
throw "Error: no CSS selector supplied"
|
||||
}
|
||||
} else {
|
||||
console.log("Unknown mode for ttsread command: " + mode)
|
||||
throw "Unknown mode for ttsread command: " + mode
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1460,7 +1483,7 @@ export async function ttscontrol(action: string) {
|
|||
if (ttsAction) {
|
||||
TTS.doAction(ttsAction)
|
||||
} else {
|
||||
console.log("Unknown text-to-speech action: " + action)
|
||||
throw new Error("Unknown text-to-speech action: " + action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ import {messageActiveTab, message} from './messaging'
|
|||
import * as config from './config'
|
||||
import * as TTS from './text_to_speech'
|
||||
import {HintSaveType} from './hinting_background'
|
||||
import Logger from './logging'
|
||||
const logger = new Logger('hinting')
|
||||
|
||||
/** Simple container for the state of a single frame's hints. */
|
||||
class HintState {
|
||||
|
@ -53,13 +55,13 @@ export function hintPage(
|
|||
state.mode = 'hint'
|
||||
modeState = new HintState()
|
||||
for (let [el, name] of izip( hintableElements, names)) {
|
||||
console.log({el, name})
|
||||
logger.debug({el, name})
|
||||
modeState.hintchars += name
|
||||
modeState.hints.push(new Hint(el, name, onSelect))
|
||||
}
|
||||
|
||||
if (modeState.hints.length) {
|
||||
console.log("HINTS", modeState.hints)
|
||||
logger.debug("hints", modeState.hints)
|
||||
modeState.focusedHint = modeState.hints[0]
|
||||
modeState.focusedHint.focused = true
|
||||
document.body.appendChild(modeState.hintHost)
|
||||
|
@ -455,7 +457,7 @@ function hintSave(hintType: HintSaveType, saveAs: boolean) {
|
|||
}
|
||||
|
||||
function selectFocusedHint() {
|
||||
console.log("Selecting hint.", state.mode)
|
||||
logger.debug("Selecting hint.", state.mode)
|
||||
const focused = modeState.focusedHint
|
||||
reset()
|
||||
focused.select()
|
||||
|
|
|
@ -70,7 +70,6 @@ import {MsgSafeKeyboardEvent} from './msgsafe'
|
|||
|
||||
*/
|
||||
export function parser(keys: MsgSafeKeyboardEvent[]) {
|
||||
console.log("hintparser", keys)
|
||||
const key = keys[0].key
|
||||
if (key === 'Escape') {
|
||||
reset()
|
||||
|
|
72
src/logging.ts
Normal file
72
src/logging.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Helper functions for logging
|
||||
*/
|
||||
|
||||
import * as Config from "./config"
|
||||
|
||||
export enum LEVEL {
|
||||
NEVER = 0, // don't use this in calls to log()
|
||||
ERROR = 1,
|
||||
WARNING = 2,
|
||||
INFO = 3,
|
||||
DEBUG = 4,
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
/**
|
||||
* Config-aware Logger class.
|
||||
*
|
||||
* @param logModule the logging module name: this is ued to look up the
|
||||
* configured/default level in the user config
|
||||
*/
|
||||
constructor(private logModule) {}
|
||||
|
||||
/**
|
||||
* Config-aware logging function.
|
||||
*
|
||||
* @param level the level of the logging - if <= configured, the message
|
||||
* will be shown
|
||||
*
|
||||
* @return logging function: this is returned as a function to
|
||||
* retain the call site
|
||||
*/
|
||||
private log(level: LEVEL) {
|
||||
let configedLevel = Config.get("logging", this.logModule) || LEVEL.WARNING
|
||||
|
||||
if (level <= configedLevel) {
|
||||
// hand over to console.log, error or debug as needed
|
||||
switch (level) {
|
||||
|
||||
case LEVEL.ERROR:
|
||||
return console.error
|
||||
case LEVEL.WARNING:
|
||||
return console.warn
|
||||
case LEVEL.INFO:
|
||||
return console.log
|
||||
case LEVEL.DEBUG:
|
||||
return console.debug
|
||||
}
|
||||
}
|
||||
|
||||
// do nothing with the message
|
||||
return function(...args) {}
|
||||
}
|
||||
|
||||
// These are all getters so that logger.debug = console.debug and
|
||||
// logger.debug('blah') translates into console.debug('blah') with the
|
||||
// filename and line correct.
|
||||
public get debug() {
|
||||
return this.log(LEVEL.DEBUG)
|
||||
}
|
||||
public get info() {
|
||||
return this.log(LEVEL.INFO)
|
||||
}
|
||||
public get warning() {
|
||||
return this.log(LEVEL.WARNING)
|
||||
}
|
||||
public get error() {
|
||||
return this.log(LEVEL.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
export default Logger
|
|
@ -1,4 +1,6 @@
|
|||
import {l, browserBg, activeTabId} from './lib/webext'
|
||||
import Logger from './logging'
|
||||
const logger = new Logger('messaging')
|
||||
|
||||
export type TabMessageType =
|
||||
"excmd_content" |
|
||||
|
@ -24,7 +26,8 @@ export type listener = (message: Message, sender?, sendResponse?) => void|Promis
|
|||
// Calls methods on obj that match .command and sends responses back
|
||||
export function attributeCaller(obj) {
|
||||
function handler(message: Message, sender, sendResponse) {
|
||||
console.log("Message:", message)
|
||||
|
||||
logger.debug(message)
|
||||
|
||||
// Args may be undefined, but you can't spread undefined...
|
||||
if (message.args === undefined) message.args = []
|
||||
|
@ -35,17 +38,17 @@ export function attributeCaller(obj) {
|
|||
|
||||
// Return response to sender
|
||||
if (response instanceof Promise) {
|
||||
console.log("Returning promise...", response)
|
||||
logger.debug("Returning promise...", response)
|
||||
sendResponse(response)
|
||||
// Docs say you should be able to return a promise, but that
|
||||
// doesn't work.
|
||||
/* return response */
|
||||
} else if (response !== undefined) {
|
||||
console.log("Returning synchronously...", response)
|
||||
logger.debug("Returning synchronously...", response)
|
||||
sendResponse(response)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${message.command}(${message.args})`, e)
|
||||
logger.error(`Error processing ${message.command}(${message.args})`, e)
|
||||
return new Promise((resolve, error)=>error(e))
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +81,7 @@ export async function messageAllTabs(type: TabMessageType, command: string, args
|
|||
let responses = []
|
||||
for (let tab of await browserBg.tabs.query({})) {
|
||||
try { responses.push(await messageTab(tab.id, type, command, args)) }
|
||||
catch (e) { console.error(e) }
|
||||
catch (e) { logger.error(e) }
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
If this turns out to be expensive there are improvements available.
|
||||
*/
|
||||
|
||||
import Logger from './logging'
|
||||
const logger = new Logger('state')
|
||||
|
||||
export type ModeName = 'normal' | 'insert' | 'hint' | 'ignore' | 'gobble' | 'input'
|
||||
class State {
|
||||
mode: ModeName = 'normal'
|
||||
|
@ -27,10 +30,10 @@ const defaults = Object.freeze(new State())
|
|||
const overlay = {} as any
|
||||
browser.storage.local.get('state').then(res=>{
|
||||
if ('state' in res) {
|
||||
console.log("Loaded initial state:", res.state)
|
||||
logger.debug("Loaded initial state:", res.state)
|
||||
Object.assign(overlay, res.state)
|
||||
}
|
||||
}).catch(console.error)
|
||||
}).catch((...args) => logger.error(...args))
|
||||
|
||||
const state = new Proxy(overlay, {
|
||||
|
||||
|
@ -45,7 +48,7 @@ const state = new Proxy(overlay, {
|
|||
|
||||
/** Persist sets to storage immediately */
|
||||
set: function(target, property, value) {
|
||||
console.log("State changed!", property, value)
|
||||
logger.debug("State changed!", property, value)
|
||||
target[property] = value
|
||||
browser.storage.local.set({state: target})
|
||||
return true
|
||||
|
|
84
src/static/logo/tridactyl.svg
Normal file
84
src/static/logo/tridactyl.svg
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48mm"
|
||||
height="48mm"
|
||||
viewBox="0 0 48 48"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="tridactyl.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.2879385"
|
||||
inkscape:cx="99.585653"
|
||||
inkscape:cy="71.989006"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1340"
|
||||
inkscape:window-height="915"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="144"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-249)">
|
||||
<circle
|
||||
style="opacity:1;fill:#1f9947;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path5917"
|
||||
cx="24"
|
||||
cy="273"
|
||||
r="19" />
|
||||
<path
|
||||
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 33.14566,271.14395 c 2.905194,-2.00919 3.118816,-2.94966 3.631495,-3.67638 0.512688,-0.72674 1.196262,0.17099 1.196262,0.94047 0,0.76946 -0.640857,5.38632 -1.196262,5.89931 -0.555405,0.51298 -1.324423,0.68397 -1.324423,0.68397 l -3.076099,-3.1634 z"
|
||||
id="path5923"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 14.032233,262.59755 c -0.755255,-3.83894 -0.725044,-4.53419 -0.513574,-5.22942 0.21147,-0.69524 -0.573988,-1.6323 -1.208407,-0.87661 -0.634409,0.7557 -3.413739,5.01783 -2.869963,5.53169 0.543785,0.51388 -0.120837,1.17889 0.936513,1.17889 1.057359,0 3.655431,-0.48365 3.655431,-0.60455 z"
|
||||
id="path5925"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 24.498991,256.83184 c 0.138757,-1.35684 6.121005,-5.9549 6.566457,-5.79613 0.44545,0.15876 -0.0039,7.94853 -0.473385,8.34084 -0.469496,0.39233 -6.093072,-2.54471 -6.093072,-2.54471 z"
|
||||
id="path5921"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 13.993892,293.90696 c 5.169539,2.7359 5.169548,-1.28246 8.331091,-2.13744 3.161542,-0.85497 5.425887,-0.76947 7.134828,-4.78784 1.708941,-4.01837 4.443248,-3.1634 5.554066,-4.70235 1.11081,-1.53895 4.051903,-7.19695 0.512679,-9.74668 -1.716916,-1.23691 -3.872923,-2.11206 -5.725261,-0.61954 -1.604661,1.29296 -2.768054,4.49046 -4.870175,7.28832 0.85447,-6.88253 4.497485,-9.76253 5.981292,-13.25208 1.100194,-2.58737 0.299066,-6.28405 -2.69158,-7.99399 -4.150603,-2.37316 -8.175477,0.98531 -8.758326,4.10386 -1.235154,6.60881 -0.939923,10.72992 -3.076099,14.57729 -0.411817,-6.03522 2.459883,-7.09235 0.512688,-11.45663 -1.068093,-2.39392 -3.845126,-3.59088 -6.237641,-2.65042 -2.3925238,0.94048 -3.7169555,4.40312 -3.5460597,7.13903 0.1708959,2.73591 2.1592337,5.05118 1.0680925,8.42148 -0.98264,3.03515 0,4.44585 1.9652792,8.03674 1.965289,3.59088 -0.683574,4.53136 3.845126,7.78025 z"
|
||||
id="path5919"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccsssscssscsccssc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -24,8 +24,7 @@ export function readText(text: string): void {
|
|||
if (window.speechSynthesis.getVoices().length === 0) {
|
||||
// should try to warn user? This apparently can happen on some machines
|
||||
// TODO: Implement when there's an error feedback mechanism
|
||||
console.log("No voice found: cannot use Text-To-Speech API")
|
||||
return
|
||||
throw new Error("No voice found: cannot use Text-To-Speech API")
|
||||
}
|
||||
|
||||
let utterance = new SpeechSynthesisUtterance(text);
|
||||
|
|
|
@ -61,17 +61,19 @@ function test_parent() {
|
|||
// single level path
|
||||
["http://example.com/path", "http://example.com/"],
|
||||
// multi-level path
|
||||
["http://example.com/path1/path2", "http://example.com/path1"],
|
||||
["http://example.com/path1/path2", "http://example.com/path1/"],
|
||||
["http://example.com/path1/path2/path3", "http://example.com/path1/path2/"],
|
||||
// subdomains
|
||||
["http://sub.example.com", "http://example.com/"],
|
||||
// subdom with path, leave subdom
|
||||
["http://sub.example.com/path", "http://sub.example.com/"],
|
||||
// trailing slash
|
||||
["http://sub.example.com/path/", "http://sub.example.com/"],
|
||||
["http://sub.example.com/path/to/", "http://sub.example.com/path/"],
|
||||
// repeated slash
|
||||
["http://example.com/path//", "http://example.com/"],
|
||||
// repeated slash
|
||||
["http://example.com//path//", "http://example.com/"],
|
||||
["http://example.com//path//", "http://example.com//"],
|
||||
["http://example.com//path//", "http://example.com//"],
|
||||
]
|
||||
|
||||
for (let [url, exp_parent] of cases) {
|
||||
|
|
|
@ -78,13 +78,10 @@ export function getUrlParent(url, count = 1) {
|
|||
return gup(parent, count - 1)
|
||||
}
|
||||
|
||||
// pathname always starts '/'
|
||||
// empty path is '/'
|
||||
if (parent.pathname !== '/') {
|
||||
// Split on '/' and remove empty substrings
|
||||
// (handles initial and trailing slashes, repeated slashes, etc.)
|
||||
let path = parent.pathname.split('/').filter(sub => sub !== "")
|
||||
path.pop()
|
||||
parent.pathname = path.join('/')
|
||||
// Remove trailing slashes and everything to the next slash:
|
||||
parent.pathname = parent.pathname.replace(/\/[^\/]*?\/*$/, '/')
|
||||
return gup(parent, count - 1)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue