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 @@
@summary
+
+ +
+
@body
-
+