diff --git a/js/background.js b/js/background.js index fecaca6..2d3cf43 100644 --- a/js/background.js +++ b/js/background.js @@ -1,196 +1,279 @@ -(function() { - const LANG_EXT_MAP = { - actionscript:['actionscript', 'as'], - apache: ['httpd', 'conf', 'htaccess'], - asciidoc: ['asciidoc'], - applescript: ['applescript'], - aspectj: ['aspectj', 'aj'], - avrasm: ['asm', 's'], - bash: ['sh', 'bash', 'zsh', 'shell'], - brainfuck: ['bf'], - clojure: ['clj'], - coffeescript:['coffee'], - cpp: ['c', 'h', 'cc', 'cpp', 'cxx', 'c++', 'hpp', 'hxx', 'h++'], - cs: ['cs'], - css: ['css'], - d: ['d', 'dd', 'di'], - dart: ['dart'], - delphi: ['pas'], - desktop: ['desktop'], - diff: ['diff', 'patch'], - dockerfile: ['dockerfile'], - dos: ['bat', 'cmd'], - elixir: ['ex', 'exs'], - erlang: ['erl', 'erlang'], - fortran: ['f', 'for', 'f90', 'f95'], - fsharp: ['fs'], - gherkin: ['feature'], - go: ['go'], - gradle: ['gradle'], - groovy: ['groovy', 'gvy', 'gy', 'gsh'], - haml: ['haml'], - handlebars: ['hbs', 'handlebars'], - haskell: ['hs'], - haxe: ['hx', 'hxml'], - http: ['http'], - ini: ['ini'], - java: ['java', 'class', 'fx'], - javascript: ['js'], - json: ['json'], - julia: ['jl'], - kotlin: ['kt', 'kts'], - less: ['less'], - lisp: ['lsp', 'lisp', 'cl', 'el', 'scm'], - livescript: ['ls'], - lua: ['lua'], - makefile: ['Makefile'], - markdown: ['md', 'markdown'], - nginx: ['nginx'], - objectivec: ['m', 'mm'], - ocaml: ['ml'], - perl: ['pl', 'pm', 'perl'], - php: ['php', 'phtml', 'phps'], - pig: ['pig'], - powershell: ['ps1', 'psm1'], - python: ['py', 'pyc'], - r: ['r'], - ruby: ['rakefile', 'gemfile', 'rb'], - scala: ['scala', 'scl', 'sca', 'scb'], - scss: ['scss', 'sass'], - smalltalk: ['st', 'sm', 'sll'], - sml: ['sml'], - sql: ['sql'], - stylus: ['styl'], - swift: ['swift'], - tex: ['tex'], - typescript: ['ts'], - vala: ['vala', 'vapi'], - vbnet: ['vb'], - vbscript: ['vbs'], - vhdl: ['vhd', 'vhdl'], - xml: ['atom', 'rss', 'vsproj', 'csproj', 'build', 'wsdl', 'config', 'xsd', 'plist', 'xib'], - yaml: ['yaml'] - }; +// background.js - const BROWSER_CONTENT = ['htm', 'html', 'xml', 'xhtml', 'shtml']; +// Mapping of language identifiers to extension arrays +const LANG_EXT_MAP = { + actionscript: ['actionscript', 'as'], + apache: ['httpd', 'conf', 'htaccess'], + asciidoc: ['asciidoc'], + applescript: ['applescript'], + aspectj: ['aspectj', 'aj'], + avrasm: ['asm', 's'], + bash: ['sh', 'bash', 'zsh', 'shell'], + brainfuck: ['bf'], + clojure: ['clj'], + coffeescript: ['coffee'], + cpp: ['c', 'h', 'cc', 'cpp', 'cxx', 'c++', 'hpp', 'hxx', 'h++'], + cs: ['cs'], + css: ['css'], + d: ['d', 'dd', 'di'], + dart: ['dart'], + delphi: ['pas'], + desktop: ['desktop'], + diff: ['diff', 'patch'], + dockerfile: ['dockerfile'], + dos: ['bat', 'cmd'], + elixir: ['ex', 'exs'], + erlang: ['erl', 'erlang'], + fortran: ['f', 'for', 'f90', 'f95'], + fsharp: ['fs'], + gherkin: ['feature'], + go: ['go'], + gradle: ['gradle'], + groovy: ['groovy', 'gvy', 'gy', 'gsh'], + haml: ['haml'], + handlebars: ['hbs', 'handlebars'], + haskell: ['hs'], + haxe: ['hx', 'hxml'], + http: ['http'], + ini: ['ini'], + java: ['java', 'class', 'fx'], + javascript: ['js'], + json: ['json'], + julia: ['jl'], + kotlin: ['kt', 'kts'], + less: ['less'], + lisp: ['lsp', 'lisp', 'cl', 'el', 'scm'], + livescript: ['ls'], + lua: ['lua'], + makefile: ['Makefile'], + markdown: ['md', 'markdown'], + nginx: ['nginx'], + objectivec: ['m', 'mm'], + ocaml: ['ml'], + perl: ['pl', 'pm', 'perl'], + php: ['php', 'phtml', 'phps'], + pig: ['pig'], + powershell: ['ps1', 'psm1'], + python: ['py', 'pyc'], + r: ['r'], + ruby: ['rakefile', 'gemfile', 'rb'], + scala: ['scala', 'scl', 'sca', 'scb'], + scss: ['scss', 'sass'], + smalltalk: ['st', 'sm', 'sll'], + sml: ['sml'], + sql: ['sql'], + stylus: ['styl'], + swift: ['swift'], + tex: ['tex'], + typescript: ['ts'], + vala: ['vala', 'vapi'], + vbnet: ['vb'], + vbscript: ['vbs'], + vhdl: ['vhd', 'vhdl'], + xml: ['atom', 'rss', 'vsproj', 'csproj', 'build', 'wsdl', 'config', 'xsd', 'plist', 'xib'], + yaml: ['yaml'] +}; - const OPTIONS_DEFAULTS = { - theme: 'sunburst', - font: 'Inconsolata', - fontSize: 'medium', - lineNumbers: true - }; +// Array of file extensions that, if detected in the content-type header, indicate browser +// content (HTML/XML) that we shouldn’t override. +const BROWSER_CONTENT = ['htm', 'html', 'xml', 'xhtml', 'shtml']; - const OPTIONS = Object.keys(OPTIONS_DEFAULTS); +// Default options +const OPTIONS_DEFAULTS = { + theme: 'sunburst', + font: 'Inconsolata', + fontSize: 'medium', + lineNumbers: true +}; +const OPTIONS = Object.keys(OPTIONS_DEFAULTS); - OPTIONS.forEach(function(option) { - var value = localStorage.getItem(option) || OPTIONS_DEFAULTS[option]; - localStorage.setItem(option, value); +// Initialize our options into chrome storage if not already set. +chrome.storage.local.get(OPTIONS, (result) => { + let updates = {}; + OPTIONS.forEach((option) => { + if (result[option] === undefined) { + updates[option] = OPTIONS_DEFAULTS[option]; + } }); - - // Reverse index - const EXT_LANG_MAP = {}; - for (var lang in LANG_EXT_MAP) { - LANG_EXT_MAP[lang].forEach(function(ext) { - EXT_LANG_MAP[ext] = lang; - }); + if (Object.keys(updates).length > 0) { + chrome.storage.local.set(updates); } +}); - function getHeaderByName(headers, name) { - var index, length = headers.length; - for (index = 0; index < length; index++) { - if (headers[index].name.toLowerCase() === name) { - return headers[index].value; - } - } - return null; - } +// Create a reverse mapping from extension to language name. +const EXT_LANG_MAP = {}; +for (const lang in LANG_EXT_MAP) { + LANG_EXT_MAP[lang].forEach((ext) => { + EXT_LANG_MAP[ext] = lang; + }); +} - function getContentTypeFromHeaders(headers) { - var contentType = getHeaderByName(headers, 'content-type'); - if (!contentType) { - return null; +// Utility functions to extract information from request headers and URLs +function getHeaderByName(headers, name) { + for (let i = 0; i < headers.length; i++) { + if (headers[i].name.toLowerCase() === name) { + return headers[i].value; } - return contentType.split(';').shift().split('/').pop().trim(); } + return null; +} - function getFilenameFromUrl(url) { - return url.split('/').pop().split('?').shift().toLowerCase(); - } +function getContentTypeFromHeaders(headers) { + const contentType = getHeaderByName(headers, 'content-type'); + if (!contentType) return null; + return contentType.split(';').shift().split('/').pop().trim(); +} - function getExtensionFromFilename(filename) { - return filename.split('.').pop(); - } +function getFilenameFromUrl(url) { + return url.split('/').pop().split('?').shift().toLowerCase(); +} - function getFragmentFromUrl(url) { - var fragment = /#ft=(\w+)/.exec(url); - return fragment && fragment[1]; - } +function getExtensionFromFilename(filename) { + return filename.split('.').pop(); +} - function detectLanguage(contentType, fragment, filename, extension) { - if (BROWSER_CONTENT.indexOf(contentType) != -1) { - return null; - } - return !!LANG_EXT_MAP[fragment] ? fragment : EXT_LANG_MAP[contentType] || - EXT_LANG_MAP[extension] || - EXT_LANG_MAP[filename]; +function getFragmentFromUrl(url) { + const fragment = /#ft=(\w+)/.exec(url); + return fragment && fragment[1]; +} + +function detectLanguage(contentType, fragment, filename, extension) { + if (BROWSER_CONTENT.indexOf(contentType) !== -1) { + return null; } + // Use the fragment if it exists; otherwise try matching the content-type, + // file extension, or even the filename. + return LANG_EXT_MAP[fragment] ? + fragment : + EXT_LANG_MAP[contentType] || + EXT_LANG_MAP[extension] || + EXT_LANG_MAP[filename]; +} - function getHighlightingCode(font, fontSize, lineNumbers, language) { - return 'document.body.style.fontFamily = "' + font + '";' + - 'document.body.style.fontSize = "' + fontSize + '";' + - 'var container = document.querySelector("pre");' + - 'container.classList.add("' + language + '");' + - 'hljs.configure({ lineNumbers: ' + lineNumbers + ' });' + - 'hljs.highlightBlock(container);' + - 'document.body.style.backgroundColor = getComputedStyle(container).backgroundColor;'; +// Injection functions – note that we convert inline code into functions so that they can be +// executed via chrome.scripting.executeScript rather than as raw inline code. + +// This function will beautify JSON code. +function beautifyInjection() { + const container = document.querySelector("pre"); + const options = { + indent_size: 2 + }; + container.textContent = js_beautify(container.textContent, options); +} + +// This function sets the font properties and activates highlighting. +function highlightInjection({ + font, + fontSize, + lineNumbers, + language +}) { + document.body.style.fontFamily = font; + document.body.style.fontSize = fontSize; + const container = document.querySelector("pre"); + if (container) { + container.classList.add(language); + hljs.configure({ + lineNumbers: lineNumbers + }); + hljs.highlightBlock(container); + document.body.style.backgroundColor = getComputedStyle(container).backgroundColor; } +} - const JS_BEUTIFY_CODE = - 'var container = document.querySelector("pre");' + - 'var options = { indent_size: 2 };' + - 'container.textContent = js_beautify(container.textContent, options);'; - - chrome.webRequest.onCompleted.addListener(function(details) { - var contentType = getContentTypeFromHeaders(details.responseHeaders); - var fragment = getFragmentFromUrl(details.url); - var filename = getFilenameFromUrl(details.url); - var extension = getExtensionFromFilename(filename); - var language = detectLanguage(contentType, fragment, filename, extension); - if (!language) { - return; - } +// Listen for completed web requests. When a main_frame request completes, check the response headers +// and URL to detect a language and, if one is found, dynamically inject CSS and JS using the new scripting API. +chrome.webRequest.onCompleted.addListener((details) => { + const contentType = getContentTypeFromHeaders(details.responseHeaders); + const fragment = getFragmentFromUrl(details.url); + const filename = getFilenameFromUrl(details.url); + const extension = getExtensionFromFilename(filename); + const language = detectLanguage(contentType, fragment, filename, extension); + if (!language) return; - var styles = [ - { file: 'css/reset.css' }, - { file: 'css/main.css' }, - { file: 'css/' + localStorage.getItem('theme') + '.css' } + // Retrieve options (theme, font, etc.) from storage. + chrome.storage.local.get(['theme', 'font', 'fontSize', 'lineNumbers'], async (options) => { + const styles = [{ + file: 'css/reset.css' + }, + { + file: 'css/main.css' + }, + { + file: `css/${options.theme}.css` + } ]; - var scripts = [ - { file: 'js/lib/highlight.js' }, - { file: 'js/languages/' + language + '.js' } + // Build our list of scripts to inject. + let scripts = [{ + file: 'js/lib/highlight.js' + }, + { + file: `js/languages/${language}.js` + } ]; + // If the language is JSON, add beautification. if (/json/.test(language)) { - scripts.push( - { file: 'js/lib/beautify.js' }, - { code: JS_BEUTIFY_CODE } - ); + scripts.push({ + file: 'js/lib/beautify.js' + }, { + func: beautifyInjection, + args: [] + }); } + // Finally, add a script to adjust font settings and set off highlighting. scripts.push({ - code: getHighlightingCode.apply(this, ['font', 'fontSize', 'lineNumbers']. - map(localStorage.getItem.bind(localStorage)).concat(language)) + func: highlightInjection, + args: [{ + font: options.font, + fontSize: options.fontSize, + lineNumbers: options.lineNumbers, + language: language + }] }); - for (var i = 0; i < styles.length; i++) { - chrome.tabs.insertCSS(details.tabId, styles[i]); + // Inject the CSS files. + for (const style of styles) { + try { + await chrome.scripting.insertCSS({ + target: { + tabId: details.tabId + }, + files: [style.file] + }); + } catch (e) { + console.error(e); + } } - (function chain(i) { - if (i == scripts.length) { return; } - chrome.tabs.executeScript(details.tabId, scripts[i], chain.bind(null, i+1)); - }(0)) - }, { urls: [''], types: ['main_frame'] }, ['responseHeaders']); -}()); + // Now inject the JS files and functions sequentially. + for (const script of scripts) { + try { + if (script.file) { + await chrome.scripting.executeScript({ + target: { + tabId: details.tabId + }, + files: [script.file] + }); + } else if (script.func) { + await chrome.scripting.executeScript({ + target: { + tabId: details.tabId + }, + func: script.func, + args: script.args + }); + } + } catch (e) { + console.error(e); + } + } + }); +}, { + urls: [''], + types: ['main_frame'] +}, ['responseHeaders']); diff --git a/js/options.js b/js/options.js index d5272ec..9ef9bcc 100644 --- a/js/options.js +++ b/js/options.js @@ -47,16 +47,26 @@ }; doc.addEventListener('DOMContentLoaded', function() { - Object.keys(options).forEach(function(name) { - var opt = options[name]; - var el = doc.querySelector(opt.selector); - el.addEventListener('change', function(e) { - var value = e.target[opt.value]; - localStorage.setItem(name, value); + // Retrieve stored options asynchronously + chrome.storage.local.get(Object.keys(options), function(storedOptions) { + Object.keys(options).forEach(function(name) { + var opt = options[name]; + var el = doc.querySelector(opt.selector); + + // Initialize values from storage + var value = storedOptions[name] !== undefined ? storedOptions[name] : opt.decode(el[opt.value]); + el[opt.value] = value; opt.render(value); + + // Update storage on change + el.addEventListener('change', function(e) { + var newValue = e.target[opt.value]; + let updateObj = {}; + updateObj[name] = newValue; + chrome.storage.local.set(updateObj); + opt.render(newValue); + }); }); - el[opt.value] = opt.decode(localStorage.getItem(name)); - el.dispatchEvent(new Event('change')); }); }); }(window.document)); diff --git a/manifest.json b/manifest.json index b422419..c146ba5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,18 +1,34 @@ { "name": "Sight", - "version": "1.20.1", + "version": "2.0.0", "description": "The Syntax Highlighter for Chrome", "homepage_url": "https://github.com/tsenart/sight", - "manifest_version": 2, + "manifest_version": 3, "background": { - "scripts": ["js/background.js"] + "service_worker": "js/background.js" }, - "web_accessible_resources": ["css/*", "fonts/*", "js/lib/*", "js/languages/*"], "options_page": "options.html", "permissions": [ "webRequest", + "scripting", + "storage" + ], + "host_permissions": [ "" ], + "web_accessible_resources": [ + { + "resources": [ + "css/*", + "fonts/*", + "js/lib/*", + "js/languages/*" + ], + "matches": [ + "" + ] + } + ], "icons": { "16": "images/icon-16.png", "48": "images/icon-48.png",