diff --git a/icons/Search.njs b/icons/Search.njs deleted file mode 100644 index 739b15fa..00000000 --- a/icons/Search.njs +++ /dev/null @@ -1,8 +0,0 @@ -export default function Search({ size }) { - return ( - - - - - ) -} \ No newline at end of file diff --git a/package.json b/package.json index 249eadc4..366d81ac 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "tailwind": "npx tailwindcss-cli build -o src/tailwind.css" }, "dependencies": { + "@docsearch/js": "^3.2.0", "@tailwindcss/typography": "^0.4.0", "glob": "^8.0.1", "nullstack-google-analytics": "github:Mortaro/nullstack-google-analytics#next", @@ -29,4 +30,4 @@ "remarkable-meta": "^1.0.1", "yaml": "^1.10.0" } -} \ No newline at end of file +} diff --git a/server.js b/server.js index f69a4457..e2d35f68 100644 --- a/server.js +++ b/server.js @@ -1,110 +1,26 @@ -import { readdirSync, readFileSync, writeFileSync } from 'fs'; +import { readdirSync } from 'fs'; import Nullstack from "nullstack"; import path from 'path'; import Application from "./src/Application"; -import prismjs from 'prismjs'; -import { Remarkable } from 'remarkable'; -import meta from 'remarkable-meta'; import 'prismjs/components/prism-jsx.min'; const context = Nullstack.start(Application); -const { worker, project, environment } = context; - -function slugify(string) { - return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z ]/g, "").toLowerCase() -} - -const locales = ['en-US', 'pt-BR'] -context.articles = {} - -for (const locale of locales) { - context.articles[locale] = {} - const articles = readdirSync(path.join(__dirname, `../i18n/${locale}`, 'articles')); - // preload files for workers - if (locale === 'en-US') { - const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations')); - worker.preload = [ - ...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1), - ...illustrations.map((illustration) => '/illustrations/' + illustration), - '/en-US.json', - '/arrow.webp', - '/stars.webp', - '/footer.webp', - '/contributors', - '/roboto-v20-latin-300.woff2', - '/roboto-v20-latin-500.woff2', - '/crete-round-v9-latin-regular.woff2', - ] - } - const map = {} - for (const article of articles) { - const content = readFileSync(path.join(__dirname, `../i18n/${locale}`, 'articles', article), 'utf-8') - // preload articles markdown - const md = new Remarkable({ - highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript') - }); - md.use(meta); - md.use((md) => { - const originalRender = md.renderer.rules.link_open; - md.renderer.rules.link_open = function () { - let result = originalRender.apply(null, arguments); - const regexp = /href="([^"]*)"/; - const href = regexp.exec(result)[1]; - if (!href.startsWith('/')) { - result = result.replace('>', ' target="_blank" rel="noopener">'); - } - return result; - }; - }); - md.use((md) => { - md.renderer.rules.heading_open = function (tokens, i) { - const { content } = tokens[i + 1]; - const { hLevel } = tokens[i]; - const id = content.toLowerCase().split(/[^a-z]/).join('-'); - return ``; - } - md.renderer.rules.heading_close = function (tokens, i) { - const { hLevel } = tokens[i]; - return ``; - } - }); - context.articles[locale][article] = { - html: md.render(content), - ...md.meta, - } - // generate word map for search - const lines = [] - let shouldSkip = false - for (const line of content.split("\n")) { - if (line.startsWith('```')) { - shouldSkip = !shouldSkip - } else if (!shouldSkip && !(line.includes('[') && line.includes(']'))) { - lines.push(line) - } - } - const words = lines.join(" ").split(" ") - const wordMap = {} - for (const word of words) { - const slug = slugify(word) - if (!slug) continue - if (!wordMap[slug]) { - wordMap[slug] = 1 - } else { - wordMap[slug]++ - } - } - const key = article.replace('.md', '') - map[key] = { - ...md.meta, - href: locale === 'en-US' ? `/${key}` : `/${locale.toLowerCase()}/${key}`, - words: wordMap - } - } - const json = environment.development ? JSON.stringify(map, null, 2) : JSON.stringify(map) - writeFileSync(`public/${locale}.json`, json) -} - +const { worker, project } = context; + +const illustrations = readdirSync(path.join(__dirname, '../public', 'illustrations')); +const articles = readdirSync(path.join(__dirname, `../i18n/en-US`, 'articles')); +worker.preload = [ + ...articles.map((article) => '/' + article.replace('.md', '')).filter((article) => article.indexOf('404') === -1), + ...illustrations.map((illustration) => '/illustrations/' + illustration), + '/arrow.webp', + '/stars.webp', + '/footer.webp', + '/contributors', + '/roboto-v20-latin-300.woff2', + '/roboto-v20-latin-500.woff2', + '/crete-round-v9-latin-regular.woff2', +] project.name = 'Nullstack'; project.domain = 'nullstack.app'; diff --git a/src/Application.njs b/src/Application.njs index 409a6e04..01e17c90 100644 --- a/src/Application.njs +++ b/src/Application.njs @@ -9,7 +9,6 @@ import Footer from './Footer'; import Header from './Header'; import Home from './Home'; import Loader from './Loader'; -import Search from './Search.njs'; import "./tailwind.css"; import Waifu from './Waifu'; @@ -34,6 +33,7 @@ class Application extends Nullstack { if (localStorage['mode']) { context.mode = localStorage['mode']; if (context.mode === 'dark') { + document.querySelector('html').setAttribute('data-theme', context.mode) context.oppositeMode = 'light'; } } @@ -53,13 +53,11 @@ class Application extends Nullstack { render({ router, mode }) { const locale = router.url.startsWith('/pt-br') ? 'pt-BR' : 'en-US'; return ( - +
- - diff --git a/src/Article.njs b/src/Article.njs index 9739550c..1b5322c3 100644 --- a/src/Article.njs +++ b/src/Article.njs @@ -1,8 +1,11 @@ -import { readFileSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; import Translatable from './Translatable'; import YAML from 'yaml'; import Arrow from '../icons/Arrow'; import './Article.scss'; +import prismjs from 'prismjs'; +import { Remarkable } from 'remarkable'; +import meta from 'remarkable-meta'; class Article extends Translatable { @@ -18,11 +21,45 @@ class Article extends Translatable { } } - static async getArticleByKey({ articles, locale, key }) { - if (articles[locale][`${key}.md`]) { - return articles[locale][`${key}.md`] + static async getArticleByKey({ locale, key }) { + await import('prismjs/components/prism-jsx.min'); + let path = `i18n/${locale}/articles/${key}.md`; + if (!existsSync(path)) { + path = `i18n/${locale}/articles/404.md`; + } + const text = readFileSync(path, 'utf-8'); + const md = new Remarkable({ + highlight: (code) => Prism.highlight(code, prismjs.languages.jsx, 'javascript') + }); + md.use(meta); + md.use((md) => { + const originalRender = md.renderer.rules.link_open; + md.renderer.rules.link_open = function () { + let result = originalRender.apply(null, arguments); + const regexp = /href="([^"]*)"/; + const href = regexp.exec(result)[1]; + if (!href.startsWith('/')) { + result = result.replace('>', ' target="_blank" rel="noopener">'); + } + return result; + }; + }); + md.use((md) => { + md.renderer.rules.heading_open = function (tokens, i) { + const { content } = tokens[i + 1]; + const { hLevel } = tokens[i]; + const id = content.toLowerCase().split(/[^a-z]/).join('-'); + return ``; + } + md.renderer.rules.heading_close = function (tokens, i) { + const { hLevel } = tokens[i]; + return ``; + } + }); + return { + html: md.render(text), + ...md.meta } - return articles[locale][`404.md`] } static async getArticlesList({ locale }) { diff --git a/src/Header.njs b/src/Header.njs index 179ea79a..65437156 100644 --- a/src/Header.njs +++ b/src/Header.njs @@ -7,7 +7,8 @@ import Brasil from "../icons/Brasil"; import Gringo from "../icons/Gringo"; import GitHub from "../icons/GitHub"; import Discord from "../icons/Discord"; -import Search from "../icons/Search"; +import docsearch from '@docsearch/js'; +import '@docsearch/css'; class Header extends Translatable { @@ -46,10 +47,16 @@ class Header extends Translatable { context.mode = context.oppositeMode; context.oppositeMode = nextOppositeMode; window.localStorage.setItem('mode', context.mode); + document.querySelector('html').setAttribute('data-theme', context.mode) } - toggleSearch({ instances }) { - instances.search.open() + startDocSearch(context) { + docsearch({ + container: context.element, + appId: 'R2IYF7ETH7', + apiKey: '599cec31baffa4868cae4e79f180729b', + indexName: 'docsearch', + }); } render({ mode, oppositeMode, locale }) { @@ -62,17 +69,17 @@ class Header extends Translatable { -
- - +
+
+
+ +
- - ) - } - -} - -export default Search; \ No newline at end of file