Skip to content

Commit

Permalink
vitepress local-search working
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangyx1998 committed Dec 23, 2023
1 parent 7976d1e commit cd2bfb7
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 1 deletion.
13 changes: 12 additions & 1 deletion config/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { defineConfig as VitePressConfig } from 'vitepress';

import nav from './nav.js';
import sidebar from './sidebar.js';
import { splitIntoSections } from './search.js';

const base = process.cwd();

Expand All @@ -36,12 +37,22 @@ export default VitePressConfig({
logo: '/x.png',
logoLink: '/',
outline: 'deep',
search: {
provider: 'local',
options: {
miniSearch: {
splitIntoSections
}
}
}
},
vite: {
build: {
chunkSizeWarningLimit: 4096,
},
plugins: [{
load(id) {
if (id === resolve(base, 'docs', 'index.md')) {
console.log(id)
return readFileSync(resolve(base, 'index.md'), 'utf-8')
}
}
Expand Down
106 changes: 106 additions & 0 deletions config/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* ---------------------------------------------------------
* Copyright (c) 2023 Yuxuan Zhang, web-dev@z-yx.cc
* This source code is licensed under the MIT license.
* You may find the full license in project root directory.
* ------------------------------------------------------ */

import { Worker, isMainThread, parentPort } from 'worker_threads';

async function worker() {
const { existsSync, readFileSync } = await import('node:fs');
const { JSDOM } = await import('jsdom');
const {
skippableTraverse: traverse,
ELEMENT_NODE,
TEXT_NODE
} = await import('../lib/traverse.js');
// Wait for message
const path = await new Promise(res => parentPort.once('message', res));
// Read file
if (!existsSync(path)) return;
const html = readFileSync(
path.replace(/(\.md)?$/, '.src.html'), 'utf-8'
);
const dom = JSDOM.fragment(html);
/**
* Stack of title hierarchy for current working section
* @type {Array<{level: number, text: string}>}
*/
const titleStack = [];
// Set of all used ids
const existingIdSet = new Set();
// Current working section
let section = { text: '', titles: [''] };
function submit() {
section.text = section.text
.replace(/\W+/sg, ' ')
.trim();
if (section.text || section.anchor) {
parentPort.postMessage(section);
}
}
// Traverse the DOM
for (const [el, skipChildren] of traverse(dom)) {
if (el.nodeType === ELEMENT_NODE) {
if (!/^H\d+$/i.test(el.tagName)) continue;
if (!el.hasAttribute('id')) continue;
const id = el.getAttribute('id');
if (existingIdSet.has(id)) {
const rel_path = path.slice(process.cwd().length + 1);
console.warn(
`\x1b[2K\r⚠️ Duplicate id ${id} in ${rel_path}`
);
continue;
}
existingIdSet.add(id);
// Submit previous section
submit();
// Pop adjacent titles depending on level
const level = parseInt(el.tagName.slice(1));
while (titleStack.length > 0) {
if (titleStack.at(-1).level >= level)
titleStack.pop();
else break;
}
titleStack.push({ level, text: el.textContent });
// Create new section
section = {
text: '',
anchor: el.getAttribute('id'),
titles: titleStack.map(_ => _.text)
};
skipChildren();
} else if (el.nodeType === TEXT_NODE) {
// Collect text content
section.text += el.textContent
}
}
// Submit last section
submit();
}

if (!isMainThread) worker();

// ====================== MAIN THREAD ======================

import { fileURLToPath } from 'node:url';
import { createAsyncGenerator } from '../lib/utility.js';

export function splitIntoSections(path) {
const worker = new Worker(fileURLToPath(import.meta.url));

const [generator, handle] = createAsyncGenerator();
worker.addListener('message', handle.yield);
worker.addListener('error', handle.throw);
worker.addListener('exit', (code) => {
worker.removeAllListeners()
if (code !== 0)
handle.throw(new Error(`Worker exit code ${code}`));
else
handle.return();
});
// Send path to worker thread
worker.postMessage(path);
// Return the generator
return generator;
}
58 changes: 58 additions & 0 deletions lib/utility.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/* ---------------------------------------------------------
* Copyright (c) 2023 Yuxuan Zhang, web-dev@z-yx.cc
* This source code is licensed under the MIT license.
* You may find the full license in project root directory.
* ------------------------------------------------------ */

// Sort source files
export function compare_path(_a, _b) {
const a = _a.split('/'), b = _b.split('/');
Expand All @@ -11,3 +17,55 @@ export function compare_path(_a, _b) {
return 0;
}
};

/**
* @returns {{
* promise: Promise,
* resolve: (value: any) => void,
* reject: (error: any) => void,
* }} a deferred promise with its resolve/reject handles
*/
export function deferPromise() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}

/**
* Create async generator with callback handlers
* @template T, R
* @returns {[AsyncGenerator<T, R>, {
* yield: (value: T) => void,
* return: (value: R) => void,
* throw: (error: any) => void,
* }]}
*/
export function createAsyncGenerator() {
let nextEvent = deferPromise();
let nextValue;
async function* generator() {
while (await nextEvent.promise) {
yield nextValue;
nextEvent = deferPromise();
}
}
return [
generator(),
{
yield(value) {
nextValue = value;
nextEvent.resolve(true);
},
return(value) {
nextValue = value;
nextEvent.resolve(false);
},
throw(error) {
nextEvent.reject(error);
}
}
];
}

0 comments on commit cd2bfb7

Please sign in to comment.