diff --git a/mkdocs.yml b/mkdocs.yml index 88b5f410a..0de53800a 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,15 +61,14 @@ theme: # Plugins plugins: - - search + - search: + prebuild_index: false # - minify: # minify_html: true # Customization extra: social: - - type: globe - link: http://struct.cc - type: github-alt link: https://github.com/squidfunk - type: twitter diff --git a/package-lock.json b/package-lock.json index e7b0915ba..9d3e0abe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1683,6 +1683,14 @@ "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } } }, "supports-color": { @@ -2549,10 +2557,9 @@ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" }, "eslint-scope": { "version": "4.0.3", @@ -5388,6 +5395,14 @@ "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } } }, "cross-spawn": { diff --git a/package.json b/package.json index ffac514db..629cec954 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "clipboard": "^2.0.0", "escape-html": "^1.0.3", + "escape-string-regexp": "^2.0.0", "lunr": "^2.3.6", "lunr-languages": "^1.1.0", "lz-string": "^1.4.4", diff --git a/src/assets/javascripts/extensions/jsx/index.ts b/src/assets/javascripts/extensions/jsx/index.ts index 72106633e..3bd603a6a 100644 --- a/src/assets/javascripts/extensions/jsx/index.ts +++ b/src/assets/javascripts/extensions/jsx/index.ts @@ -52,9 +52,9 @@ type Child = Child[] | Element | Text | string | number */ function appendChild(el: Element, child: Child): void { - /* Handle primitive types */ + /* Handle primitive types (including raw HTML) */ if (typeof child === "string" || typeof child === "number") { - el.appendChild(new Text(child.toString())) + el.innerHTML += child.toString() /* Handle nodes */ } else if (child instanceof Node) { diff --git a/src/assets/javascripts/modules/search/_/index.ts b/src/assets/javascripts/modules/search/_/index.ts index 647b29344..7b8f1c2bc 100644 --- a/src/assets/javascripts/modules/search/_/index.ts +++ b/src/assets/javascripts/modules/search/_/index.ts @@ -28,6 +28,10 @@ import { SectionDocument, setupSearchDocumentMap } from "../document" +import { + SearchHighlightFactoryFn, + setupSearchHighlighter +} from "../highlight" /* ---------------------------------------------------------------------------- * Types @@ -113,6 +117,11 @@ export class Search { */ protected documents: SearchDocumentMap + /** + * Search highlight factory function + */ + protected highlight: SearchHighlightFactoryFn + /** * The lunr search index */ @@ -124,8 +133,9 @@ export class Search { * @param index - Search index * @param options - Options */ - public constructor({ docs, options, index }: SearchIndex) { + public constructor({ config, docs, options, index }: SearchIndex) { this.documents = setupSearchDocumentMap(docs) + this.highlight = setupSearchHighlighter(config) /* If no index was given, create it */ if (typeof index === "undefined") { @@ -171,16 +181,24 @@ export class Search { * page. For this reason, section results are grouped within their respective * articles which are the top-level results that are returned. * + * Rogue control characters must be filtered before handing the query to the + * search index, as lunr will throw otherwise. + * * @param query - Query string * * @return Search results */ public search(query: string): SearchResult[] { - const groups = this.index.search(query - .replace(/\s+[+-](?:\s+|$)/, "") /* Filter rogue quantifiers */ - ) + query = query + .replace(/(?:^|\s+)[+-:~^]+(?=\s+|$)/g, "") + .trim() - /* Group sections by containing article */ + /* Abort early, if query is empty */ + if (!query) + return [] + + /* Group sections by containing article */ + const groups = this.index.search(query) .reduce((results, result) => { const document = this.documents.get(result.ref) if (typeof document !== "undefined") { @@ -195,11 +213,14 @@ export class Search { return results }, new Map()) + /* Create highlighter for query */ + const fn = this.highlight(query) + /* Map groups to search documents */ return [...groups].map(([ref, sections]) => ({ - article: this.documents.get(ref) as ArticleDocument, + article: fn(this.documents.get(ref) as ArticleDocument), sections: sections.map(section => { - return this.documents.get(section.ref) as SectionDocument + return fn(this.documents.get(section.ref) as SectionDocument) }) })) } diff --git a/src/assets/javascripts/modules/search/document/index.ts b/src/assets/javascripts/modules/search/document/index.ts index 2c462dfaf..4ed0729f7 100644 --- a/src/assets/javascripts/modules/search/document/index.ts +++ b/src/assets/javascripts/modules/search/document/index.ts @@ -61,7 +61,7 @@ export type SearchDocumentMap = Map * ------------------------------------------------------------------------- */ /** - * Build a search document mapping + * Create a search document mapping * * @param docs - Search index documents * diff --git a/src/assets/javascripts/workers/packer/main/index.ts b/src/assets/javascripts/workers/packer/main/index.ts index 284b94e58..7a5e6b5f3 100644 --- a/src/assets/javascripts/workers/packer/main/index.ts +++ b/src/assets/javascripts/workers/packer/main/index.ts @@ -21,10 +21,45 @@ * IN THE SOFTWARE. */ -import { compress, decompress } from "lz-string" +import { + compress, + compressToUTF16, + decompress, + decompressFromUTF16 +} from "lz-string" import { PackerMessage, PackerMessageType } from "../_" +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Determine methods for packing and unpacking + * + * While all Webkit-based browsers can store invalid UTF-16 strings in local + * storage, other browsers may only store valid UTF-16 strings. + * + * @see https://bit.ly/2Q1ArhU - LZ-String documentation + */ +const isWebkit = navigator.userAgent.includes("AppleWebKit") + +/* ------------------------------------------------------------------------- */ + +/** + * Method for packing + */ +const pack = isWebkit + ? compress + : compressToUTF16 + +/** + * Method for unpacking + */ +const unpack = isWebkit + ? decompress + : decompressFromUTF16 + /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ @@ -43,14 +78,14 @@ export function handler(message: PackerMessage): PackerMessage { case PackerMessageType.STRING: return { type: PackerMessageType.PACKED, - data: compress(message.data) + data: pack(message.data) } /* Unpack a packed string */ case PackerMessageType.PACKED: return { type: PackerMessageType.STRING, - data: decompress(message.data) + data: unpack(message.data) } } } diff --git a/src/assets/javascripts/workers/search/_/index.ts b/src/assets/javascripts/workers/search/_/index.ts index 178d62468..81222f76b 100644 --- a/src/assets/javascripts/workers/search/_/index.ts +++ b/src/assets/javascripts/workers/search/_/index.ts @@ -20,7 +20,10 @@ * IN THE SOFTWARE. */ -import { SearchIndex, SearchResult } from "../../../modules" +import { Subject } from "rxjs" + +import { SearchIndex, SearchResult } from "modules" +import { watchWorker } from "utilities" /* ---------------------------------------------------------------------------- * Types