All repositories

astro-lunr

License

Lunr integration for Astro; client-side search for statically hosted pages.

2022-05-14 17:06 #1: fixed base path replacement problem
Siver K. Volle 3f44311
2022-05-14 17:06 #1: fixed base path replacement problem master Siver K. Volle 3f44311
2022-04-18 19:51 initial steps to turn astro-lunr into its own repo Siver K. Volle 9983ab3
2022-04-18 18:39 better lunr in dev-mode; better file diffs Siver K. Volle 182546c
2022-04-16 00:25 Included readme and licenses; made the plugins into npm-modules; removed more dead-links and ui/ux-bugs Siver K. Volle 077a354
2022-04-15 17:21 Multi-repo support; refactored astro-git and astro-lunr into independent plugins Siver K. Volle adfedbe
2022-04-14 10:00 lunr-based indexing and search integration; assorted minor ux/ui improvements Siver K. Volle 146ebb5
astro-lunr / plugin.mjs
158 lines (150 sloc) | 4.7 KB
1 import fs from 'node:fs';
2 import path from 'node:path';
3 import {rehype} from 'rehype'
4 import lunr from 'lunr';
5 import crypto from 'node:crypto';
6
7 function traverseReplace(replace, tree) {
8 return transform(tree, null, null);
9 function transform(node, index, parent) {
10 let replacement = replace(node, index, parent);
11 if(replacement){
12 return replacement;
13 } else {
14 if ('children' in node) {
15 return {
16 ...node,
17 children: node.children.flatMap(
18 (child, index) => transform(child, index, node)
19 )
20 };
21 } return node;
22 }
23 }
24 }
25
26 function parseLunrDocument(lunrTree){
27 let doc = {};
28 traverseReplace((node => {
29 if(node.type === "element"){
30 switch(node.tagName){
31 case "lunr-field": {
32 doc[node.properties.name] = node.properties.value;
33 break;
34 }
35 case "lunr-text": {
36 doc[node.properties.name] = node.children.filter(n => n.type === "text").map(n => n.value).join("\n");
37 break;
38 }
39 }
40 }
41 }), lunrTree)
42 return doc;
43 }
44
45 function indexLunrDocuments(canonicalUrl, addToBulk){
46 return () => traverseReplace.bind(null, (node) => {
47 if(node.type === "element" && node.tagName === "lunr-document"){
48 let doc = parseLunrDocument(node);
49 doc["canonicalUrl"] = canonicalUrl;
50 doc["__index__"] = node.properties.index;
51 addToBulk(doc);
52 return []
53 }
54 });
55 }
56
57
58 export function createVitePlugin({ config }) {
59 return {
60 name: '@siverv/astro-lunr:dev-server',
61 configureServer(viteServer) {
62 viteServer.middlewares.use((req, res, next) => {
63 if(req.url.endsWith("/idx.json") || req.url.endsWith("/docs.json")){
64 let path = req.url;
65 if(config.base && path.startsWith(config.base)){
66 path = path.replace(config.base, "");
67 }
68 let preBuiltPath = new URL(path, config.outDir);
69 try {
70 var stat = fs.statSync(preBuiltPath);
71 } catch(err){
72 err.message = "Could not find pre-built lunr-files - search is not available without first building your astro-pages at least once. " + err.toString();
73 throw err;
74 }
75 res.writeHead(200, {
76 'Content-Type': 'application/json',
77 'Content-Length': stat.size
78 });
79 return fs.createReadStream(preBuiltPath).pipe(res);
80 }
81 return next();
82 });
83 },
84 };
85 }
86
87 function getViteConfiguration(config) {
88 return {
89 plugins: [createVitePlugin(config)]
90 };
91 }
92
93
94 export default function createPlugin({pathFilter, subDir, documentFilter, initialize, mapDocument, verbose}){
95 let config = {};
96 let pathsToIndex = []
97 return {
98 name: '@siverv/astro-lunr:plugin',
99 hooks: {
100 'astro:config:setup': (options) => {
101 if(options.command === "dev"){
102 options.addRenderer({
103 name: '@siverv/astro-lunr:renderer',
104 serverEntrypoint: '@siverv/astro-lunr/server/renderer.js',
105 });
106 options.updateConfig({ vite: getViteConfiguration(options) });
107 }
108 },
109 'astro:build:done': async ({pages, dir}) => {
110 let indexMap = new Map();
111 const addToIndexMap = (doc) => {
112 if(documentFilter && !documentFilter(doc)){
113 return;
114 }
115 const {__index__: index, ...rest} = doc;
116 if(!indexMap.has(index)){
117 indexMap.set(index, []);
118 }
119 indexMap.get(index).push({
120 ...rest,
121 id: doc["id"] || crypto.createHash('md5').update(doc["canonicalUrl"]).digest('hex')
122 });
123 }
124 let documents = [];
125 for(let {pathname} of pages) {
126 if(pathFilter && !pathFilter(pathname)){
127 continue;
128 }
129 let url = new URL((pathname ? pathname + "/" : "") + "index.html", dir);
130 let content = fs.readFileSync(url, "utf-8");
131 let newDocuments = [];
132 let hyped = await rehype()
133 .use(indexLunrDocuments(pathname, (doc) => newDocuments.push(doc)))
134 .process(content);
135 if(newDocuments.length > 0) {
136 if(verbose){
137 console.log(`Indexing ${newDocuments.length} doc(s) from ${pathname}`);
138 }
139 fs.writeFileSync(url, String(hyped));
140 newDocuments.forEach(addToIndexMap)
141 }
142 }
143 for(let [index, documents] of indexMap){
144 const idx = lunr(function () {
145 initialize(this, lunr);
146 documents.forEach(doc => this.add(doc));
147 })
148 if(mapDocument){
149 documents = documents.map(mapDocument);
150 }
151 fs.mkdirSync(new URL(path.join(subDir || "", index || ""), dir), { recursive: true });
152 fs.writeFileSync(new URL(path.join(subDir || "", index || "", 'idx.json'), dir), JSON.stringify(idx));
153 fs.writeFileSync(new URL(path.join(subDir || "", index || "", 'docs.json'), dir), JSON.stringify(documents));
154 }
155 }
156 }
157 }
158 }