diff --git a/lib/search.js b/lib/search.js
new file mode 100644
index 0000000..60bc571
--- /dev/null
+++ b/lib/search.js
@@ -0,0 +1,33 @@
+async function loadSearchIndex() {
+ const response = await fetch('./search-index.json');
+ return response.json();
+}
+
+function search(query, index) {
+ return index.filter(entry =>
+ entry.title.toLowerCase().includes(query.toLowerCase()) ||
+ entry.content.toLowerCase().includes(query.toLowerCase())
+ );
+}
+
+document.getElementById('search-bar').addEventListener('input', async (event) => {
+ const query = event.target.value;
+ const resultsContainer = document.getElementById('search-results');
+ resultsContainer.innerHTML = '';
+
+ if (query.length < 3) return; // Minimum query length
+
+ const index = await loadSearchIndex();
+ const results = search(query, index);
+
+ results.forEach(result => {
+ const resultElement = document.createElement('div');
+ resultElement.innerHTML = `
+
+ ${result.title}
+ ${result.content.substring(0, 200)}...
+
+ `;
+ resultsContainer.appendChild(resultElement);
+ });
+});
\ No newline at end of file
diff --git a/package.json b/package.json
index 158cab8..b320eaa 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,11 @@
"vitest": "3.2.4",
"vitest-environment-clarinet": "2.3.0"
},
+ "homepage": "https://github.com/clarity-lang/book#readme",
+ "description": "",
"dependencies": {
- "marked-highlight": "^2.2.2"
+ "fs": "^0.0.1-security",
+ "markdown-it": "^14.1.0",
+ "path": "^0.12.7"
}
}
diff --git a/scripts/build.js b/scripts/build.js
index 486a8d4..c5e1dc4 100755
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -1,89 +1,78 @@
#!/usr/bin/env node
-import fs from "node:fs/promises";
-import path from "node:path";
+const fs = require('fs/promises');
+const path = require('path');
+const fetch = require('node-fetch');
+const { build_page, link_page } = require('../lib/builder');
+const generateSearchIndex = require('./genSearchIndex');
import { build_page, link_page } from "../lib/builder.js";
import { fileURLToPath } from "node:url";
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
+const [input,output] = process.argv;
+const input_path = path.resolve(__dirname,input || '../src');
+const output_path = path.resolve(__dirname,output || '../build');
+const lib_path = path.resolve(__dirname, '../lib');
-const [, , input, output] = process.argv;
+async function readdir_sorted(dir,subdir)
+ {
+ let entries = [];
+ for await (const d of await fs.opendir(dir))
+ {
+ const entry = path.join(dir, d.name);
+ if (d.isDirectory())
+ entries = entries.concat(await readdir_sorted(entry,true));
+ else if (d.isFile())
+ entries.push({entry,name:d.name});
+ }
+ return subdir ? entries : entries.sort((a,b) => a.entry.localeCompare(b.entry));
+ }
-const input_path = path.resolve(__dirname, input || "../src");
-const output_path = path.resolve(__dirname, output || "../build");
+async function build(input_path,output_path,template)
+ {
+ console.log('Starting build');
+ const summary = await fs.readFile(path.resolve(__dirname,'../src/SUMMARY.md'),'utf8');
+ for (const {entry,name} of await readdir_sorted(input_path))
+ {
+ console.log(entry);
+ await fs.mkdir(path.join(output_path,entry.substr(input_path.length,entry.length-input_path.length-name.length)),{recursive:true});
+ const output_file_path = path.join(output_path,entry.substr(input_path.length));
-async function readdir_sorted(dir, subdir) {
- let entries = [];
- for await (const d of await fs.opendir(dir)) {
- const entry = path.join(dir, d.name);
- if (d.isDirectory())
- entries = entries.concat(await readdir_sorted(entry, true));
- else if (d.isFile()) entries.push({ entry, name: d.name });
- }
- return subdir
- ? entries
- : entries.sort((a, b) => a.entry.localeCompare(b.entry));
-}
+ entry.substr(-3) === '.md'
+ ? fs.writeFile(output_file_path.slice(0,-3)+'.html',await build_page(entry,{template,summary}),'utf8')
+ : fs.copyFile(entry,output_file_path);
+ if (name === 'title-page.md')
+ fs.writeFile(path.join(output_path,'index.html'),await build_page(entry,{template,summary}),'utf8');
+ }
-async function build(input_path, output_path, template) {
- console.log("Starting build");
- const summary = await fs.readFile(
- path.resolve(__dirname, "../src/SUMMARY.md"),
- "utf8"
- );
+ await fs.mkdir(output_path, { recursive: true });
+ // Copy search.js to the build directory
+ await fs.copyFile(path.join(lib_path, 'search.js'), path.join(output_path, 'search.js'));
- for (const { entry, name } of await readdir_sorted(input_path)) {
- console.log(entry);
- await fs.mkdir(
- path.join(
- output_path,
- entry.substr(
- input_path.length,
- entry.length - input_path.length - name.length
- )
- ),
- { recursive: true }
- );
- const output_file_path = path.join(
- output_path,
- entry.substr(input_path.length)
- );
+ console.log('Generating search index...');
+ await generateSearchIndex();
- entry.substr(-3) === ".md"
- ? fs.writeFile(
- output_file_path.slice(0, -3) + ".html",
- await build_page(entry, { template, summary }),
- "utf8"
- )
- : fs.copyFile(entry, output_file_path);
- if (name === "title-page.md")
- fs.writeFile(
- path.join(output_path, "index.html"),
- await build_page(entry, { template, summary }),
- "utf8"
- );
- }
- console.log("Linking chapters");
- const chapter_files = (await readdir_sorted(output_path)).filter((d) =>
- d.name.match(/^ch[0-9]+-[0-9]+-[\S]+\.html$/)
- );
- chapter_files.forEach(async ({ entry }, index) => {
- const [prev, next] = [chapter_files[index - 1], chapter_files[index + 1]];
- if (prev || next)
- fs.writeFile(
- entry,
- await link_page(entry, prev && prev.name, next && next.name),
- "utf8"
- );
- });
-}
+ console.log('Linking chapters');
+ const chapter_files = (await readdir_sorted(output_path)).filter(d => d.name.match(/^ch[0-9]+-[0-9]+-[\S]+\.html$/));
+ chapter_files.forEach(async ({entry}, index) =>
+ {
+ const [prev, next] = [chapter_files[index-1], chapter_files[index+1]];
+ if (prev || next)
+ fs.writeFile(entry,await link_page(entry,prev && prev.name,next && next.name),'utf8');
+ });
+ const repl_file = path.join(output_path,'repl/clarity_repl_bg.wasm');
+ let repl_exists = false;
+ try
+ {
+ repl_exists = (await fs.stat(repl_file)).code !== 'ENOENT';
+ }
+ catch (error){}
+ if (!repl_exists)
+ {
+ console.log(`Downloading REPL WASM from ${repl_binary_url}`);
+ await fs.mkdir(path.join(output_path,'repl'));
+ fs.writeFile(repl_file,Buffer.from(await (await fetch(repl_binary_url)).arrayBuffer()));
+ }
+ }
-fs.readFile(path.resolve(__dirname, "../templates/base.html"), "utf8").then(
- (template) => build(input_path, output_path, template)
-);
-
-fs.copyFile(
- path.resolve(__dirname, "../public/clarinet-worker.js"),
- path.resolve(__dirname, "../build/clarinet-worker.js")
-);
+fs.readFile(path.resolve(__dirname,'../templates/base.html'),'utf8')
+ .then(template => build(input_path,output_path,template));
diff --git a/scripts/genSearchIndex.js b/scripts/genSearchIndex.js
new file mode 100644
index 0000000..ee1992c
--- /dev/null
+++ b/scripts/genSearchIndex.js
@@ -0,0 +1,36 @@
+const fs = require('fs');
+const path = require('path');
+const markdownIt = require('markdown-it')();
+
+const srcDir = path.join(__dirname, '../src');
+const outputFile = path.join(__dirname, '../build/search-index.json');
+
+async function generateSearchIndex() {
+ const index = [];
+
+ function processMarkdownFile(filePath) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const html = markdownIt.render(content);
+ const titleMatch = content.match(/^#\s+(.*)/m);
+ const title = titleMatch ? titleMatch[1] : 'Untitled';
+ const relativePath = path.relative(srcDir, filePath);
+
+ index.push({
+ title,
+ content: html.replace(/<[^>]*>/g, ''), // Strip HTML tags
+ path: relativePath.replace(/\.md$/, '.html'),
+ });
+ }
+
+ fs.readdirSync(srcDir).forEach(file => {
+ if (file.endsWith('.md')) {
+ processMarkdownFile(path.join(srcDir, file));
+ }
+ });
+
+ await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
+ fs.writeFileSync(outputFile, JSON.stringify(index, null, 2));
+ console.log('Search index generated:', outputFile);
+}
+
+module.exports = generateSearchIndex;
\ No newline at end of file
diff --git a/src/ch06-03-unwrap-flavours.md b/src/ch06-03-unwrap-flavours.md
index c3e7028..26bf624 100644
--- a/src/ch06-03-unwrap-flavours.md
+++ b/src/ch06-03-unwrap-flavours.md
@@ -5,7 +5,7 @@ in a slightly different manner.
`unwrap!` takes an `optional` or `response` as the first input and a throw value
as the second input. It follows the same unwrapping behaviour of `try!` , but
-instead of propagating the `none` or the `err` it will return the the throw
+instead of propagating the `none` or the `err` it will return the throw
value instead.
```Clarity
diff --git a/src/style.css b/src/style.css
index 5166bd3..0bf5afe 100644
--- a/src/style.css
+++ b/src/style.css
@@ -668,4 +668,29 @@ pre,
body.playground .code .result{
flex-basis: 50%;
}
+}
+
+#search-bar-container{
+ display: block;
+ justify-content: center;
+ align-items: center;
+ margin: 0 0 2em;
+}
+
+#search-bar{
+ display: block;
+}
+
+#search-results{
+ display:block;
+ margin: 2em 0;
+}
+
+input[type="text"]{
+ display:block;
+ border: 1px solid #777;
+ border-radius: 1em;
+ padding: 1em;
+ width: 100%;
+ box-sizing: border-box;
}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index 26526a1..bb4cf56 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,7 +1,8 @@
-
-
+
+
+
@title - Clarity Book
@@ -12,8 +13,12 @@
+