')
- $div.html(this.mdConverter.makeHtml(md))
- $div.remove('style')
-
- return jieba.cutForSearch($div.html() || '').join(' ')
+ private async getHTML(filename: string) {
+ return this.mdConverter.makeHtml(
+ await ejs.renderFile(filename, {
+ youtube: (id: string) =>
+ this.$('
')
+ .html(
+ /* html */ `
+
+
+
`
+ )
+ .html(),
+ })
+ )
}
}
diff --git a/packages/nuxt/scripts/prepare.ts b/packages/nuxt/scripts/prepare.ts
index fa1eb36..3594391 100644
--- a/packages/nuxt/scripts/prepare.ts
+++ b/packages/nuxt/scripts/prepare.ts
@@ -4,8 +4,9 @@ import yaml from 'js-yaml'
import rimraf from 'rimraf'
import { Database, IMatter } from '../server/db'
+import { builtPath, dstMediaPath } from '../server/dir'
import { THEME_FILENAME } from '../server/theme'
-import { builtPath, dstMediaPath, srcPath } from './dir'
+import { srcPath } from './dir'
import { MakeHTML } from './make-html'
async function main() {
@@ -17,40 +18,38 @@ async function main() {
await fs.copyFile(srcPath(THEME_FILENAME), builtPath(THEME_FILENAME))
- const db = new Database(builtPath('data.db'))
+ const db = new Database()
db.init()
const files = await fg('**/*.md', {
- cwd: srcPath('_post'),
+ cwd: srcPath('_posts'),
})
- // eslint-disable-next-line array-callback-return
- files.map((f) => {
- const path = f.replace(/\.md$/, '')
- let markdown = fs.readFileSync(srcPath('_post', f), 'utf8')
+ const makeHtml = new MakeHTML()
- const [h, c = ''] = markdown.substr(4).split(/---\n(.*)$/s)
- const header = yaml.safeLoad(h, {
- schema: yaml.JSON_SCHEMA,
- }) as IMatter
- markdown = c
+ await Promise.all(
+ files.map(async (f) => {
+ const path = f.replace(/\.md$/, '')
+ const markdown = fs.readFileSync(srcPath('_posts', f), 'utf8')
- const makeHtml = new MakeHTML(path)
+ const [h] = markdown.substr(4).split(/---\n(.*)$/s)
+ const header = yaml.safeLoad(h) as IMatter
- const content = makeHtml.clean(markdown)
- const excerptHtml = makeHtml.parse(
- markdown.split(//)[0]
- )
- const contentHtml = makeHtml.parse(markdown)
+ const r = await makeHtml.parse(srcPath('_posts', f))
+ const contentHtml = r.html()
+ const excerptHtml = contentHtml.split(
+ //
+ )[0]
- db.insert({
- ...header,
- path,
- content,
- excerptHtml,
- contentHtml,
+ db.insert({
+ ...header,
+ path,
+ content: r.clean(),
+ excerptHtml,
+ contentHtml,
+ })
})
- })
+ )
}
if (require.main === module) {
diff --git a/packages/nuxt/server/db.ts b/packages/nuxt/server/db.ts
index 2310a11..5d1f6b8 100644
--- a/packages/nuxt/server/db.ts
+++ b/packages/nuxt/server/db.ts
@@ -1,6 +1,8 @@
import sqlite3 from 'better-sqlite3'
import * as z from 'zod'
+import { builtPath } from './dir'
+
const zMatter = z.object({
date: z.date().optional(),
createdAt: z.date().optional(),
@@ -31,8 +33,8 @@ export type IPost = z.infer
export class Database {
db: sqlite3.Database
- constructor(public filepath: string, opts?: sqlite3.Options) {
- this.db = sqlite3(filepath, opts)
+ constructor(opts?: sqlite3.Options) {
+ this.db = sqlite3(builtPath('data.db'), opts)
}
public init(): void {
@@ -40,8 +42,8 @@ export class Database {
CREATE TABLE IF NOT EXISTS files (
[path] TEXT PRIMARY KEY,
[date] INT, -- epoch milli
- [createdAt] INT, -- epoch milli
- [updatedAt] INT, -- epoch milli
+ createdAt INT, -- epoch milli
+ updatedAt INT, -- epoch milli
priority INT,
-- q.title
[image] TEXT,
@@ -83,20 +85,158 @@ export class Database {
this.db.transaction(() => {
// eslint-disable-next-line array-callback-return
ps.map((p) => {
- p = zPost.parse(p)
+ const {
+ path,
+ date,
+ createdAt,
+ updatedAt,
+ priority,
+ image,
+ excerptHtml,
+ contentHtml,
+ title,
+ tag,
+ category,
+ content,
+ } = zPost.parse(p)
fStmt.run({
- ...p,
- date: p.date ? p.date.getTime() : undefined,
- createdAt: p.createdAt ? p.createdAt.getTime() : undefined,
- updatedAt: p.updatedAt ? p.updatedAt.getTime() : undefined,
+ path,
+ priority,
+ image,
+ excerptHtml,
+ contentHtml,
+ date: date ? date.getTime() : null,
+ createdAt: createdAt ? createdAt.getTime() : null,
+ updatedAt: updatedAt ? updatedAt.getTime() : null,
})
qStmt.run({
- ...p,
- tag: p.tag ? p.tag.join(' ') : undefined,
+ path,
+ title,
+ category,
+ content,
+ tag: tag ? tag.join(' ') : undefined,
})
})
})()
}
+
+ public get(id: string) {
+ const r = this.db
+ .prepare(
+ /* sql */ `
+ SELECT
+ [date],
+ title,
+ [image],
+ tag,
+ category,
+ excerptHtml excerpt,
+ contentHtml content
+ FROM files f
+ JOIN q ON q.path = f.path
+ WHERE f.path = @id
+ `
+ )
+ .get({ id })
+
+ if (!r) {
+ return null
+ }
+
+ return {
+ ...r,
+ date: r.date ? new Date(r.date) : undefined,
+ tag: r.tag ? r.tag.split(/ /g) : undefined,
+ } as {
+ date?: Date
+ title: string
+ image?: string
+ tag?: string[]
+ category?: string
+ excerpt: string
+ content: string
+ }
+ }
+
+ public q(
+ q: string,
+ opts: {
+ offset: number
+ limit: number
+ }
+ ) {
+ return this._search(q, opts, 'RANK')
+ }
+
+ public by(
+ q: string,
+ opts: {
+ offset: number
+ limit: number
+ }
+ ) {
+ return this._search(
+ q,
+ opts,
+ 'priority DESC, [date] DESC, createdAt DESC, updatedAt DESC'
+ )
+ }
+
+ private _search(
+ q: string,
+ opts: {
+ offset: number
+ limit: number
+ },
+ orderedBy: string
+ ) {
+ const fromSegment = /* sql */ `
+ FROM files f
+ JOIN q ON q.path = f.path
+ ${q ? `WHERE q = @q` : ''}
+ `
+
+ const count =
+ this.db
+ .prepare(
+ /* sql */ `
+ SELECT COUNT(f.path) [count]
+ ${fromSegment}
+ `
+ )
+ .get({ q })?.count || 0
+
+ const result = this.db
+ .prepare(
+ /* sql */ `
+ SELECT
+ f.path [path],
+ [date],
+ title,
+ [image],
+ tag,
+ category,
+ excerptHtml excerpt
+ ${fromSegment}
+ ORDER BY ${orderedBy}
+ LIMIT ${opts.limit} OFFSET ${opts.offset}
+ `
+ )
+ .all({ q }) as {
+ path: string
+ date?: Date
+ title: string
+ image?: string
+ tag?: string[]
+ category?: string
+ excerpt: string
+ }[]
+
+ return {
+ result,
+ count,
+ }
+ }
}
diff --git a/packages/nuxt/server/dir.ts b/packages/nuxt/server/dir.ts
new file mode 100644
index 0000000..d3f51cb
--- /dev/null
+++ b/packages/nuxt/server/dir.ts
@@ -0,0 +1,7 @@
+import path from 'path'
+
+export const builtPath = (...ps: string[]): string =>
+ path.join(__dirname, '../built', ...ps)
+
+export const dstMediaPath = (...ps: string[]): string =>
+ path.join(__dirname, '../static/media', ...ps)
diff --git a/packages/nuxt/server/index.ts b/packages/nuxt/server/index.ts
new file mode 100644
index 0000000..9b1dc03
--- /dev/null
+++ b/packages/nuxt/server/index.ts
@@ -0,0 +1,58 @@
+import express from 'express'
+import * as z from 'zod'
+
+import { Database } from './db'
+
+const app = express()
+const db = new Database({ readonly: true })
+
+app.get('/', (req, res) => {
+ const { id } = z
+ .object({
+ id: z.string(),
+ })
+ .parse(req.query)
+
+ const r = db.get(id)
+ if (!r) {
+ res.sendStatus(404)
+ return
+ }
+
+ res.json(r)
+})
+
+app.get('/q', (req, res) => {
+ const { q, tag, page: _page = '1' } = z
+ .object({
+ q: z.string().optional(),
+ tag: z.string().optional(),
+ page: z
+ .string()
+ .refine((s) => /^\d+$/.test(s))
+ .optional(),
+ })
+ .parse(req.query)
+
+ const perPage = 5
+ const offset = (parseInt(_page) - 1) * perPage
+
+ if (q) {
+ res.json(
+ db.q(`${q} ${tag ? `tag:${tag}` : ''}`, {
+ offset,
+ limit: perPage,
+ })
+ )
+ return
+ }
+
+ res.json(
+ db.by(tag ? `tag:${tag}` : '', {
+ offset,
+ limit: perPage,
+ })
+ )
+})
+
+export default app
diff --git a/packages/nuxt/static/webcomponents.js b/packages/nuxt/static/webcomponents.js
new file mode 100644
index 0000000..1e95c92
--- /dev/null
+++ b/packages/nuxt/static/webcomponents.js
@@ -0,0 +1,11 @@
+/* global WebComponents */
+WebComponents.waitFor(() => {
+ // At this point we are guaranteed that all required polyfills have
+ // loaded, and can use web components API's.
+ // The standard pattern is to load element definitions that call
+ // `customElements.define` here.
+ // Note: returning the import's promise causes the custom elements
+ // polyfill to wait until all definitions are loaded and then upgrade
+ // the document in one batch, for better performance.
+ return import('https://unpkg.com/@patarapolw/make-html-x-card')
+})
diff --git a/packages/nuxt/tsconfig.json b/packages/nuxt/tsconfig.json
index 65f9bbc..1270666 100644
--- a/packages/nuxt/tsconfig.json
+++ b/packages/nuxt/tsconfig.json
@@ -3,11 +3,7 @@
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
- "lib": [
- "ESNext",
- "ESNext.AsyncIterable",
- "DOM"
- ],
+ "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
@@ -16,21 +12,10 @@
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
- "~/*": [
- "./*"
- ],
- "@/*": [
- "./*"
- ]
+ "~/*": ["./*"],
+ "@/*": ["./*"]
},
- "types": [
- "@types/node",
- "@nuxt/types"
- ]
+ "types": ["@types/node", "@nuxt/types", "@nuxtjs/axios"]
},
- "exclude": [
- "node_modules",
- ".nuxt",
- "dist"
- ]
+ "exclude": ["node_modules", ".nuxt", "dist"]
}
diff --git a/packages/nuxt/yarn.lock b/packages/nuxt/yarn.lock
index 4a82cd4..f80aa68 100644
--- a/packages/nuxt/yarn.lock
+++ b/packages/nuxt/yarn.lock
@@ -1404,6 +1404,11 @@
dependencies:
"@types/node" "*"
+"@types/ejs@^3.0.5":
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.0.5.tgz#95a3a1c3d9603eba80fe67ff56da1ba275ef2eda"
+ integrity sha512-k4ef69sS4sIqAPW9GoBnN+URAON2LeL1H0duQvL4RgdEBna19/WattYSA1qYqvbVEDRTSWzOw56tCLhC/m/IOw==
+
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -2273,6 +2278,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+async@0.9.x:
+ version "0.9.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
+ integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
+
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@@ -3749,6 +3759,13 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
+ejs@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.5.tgz#aed723844dc20acb4b170cd9ab1017e476a0d93b"
+ integrity sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w==
+ dependencies:
+ jake "^10.6.1"
+
electron-to-chromium@^1.3.621:
version "1.3.626"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.626.tgz#48acdf322be07feb2c1330ba05e4bf6327f721a3"
@@ -4417,6 +4434,13 @@ file-uri-to-path@1.0.0:
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+filelist@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb"
+ integrity sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==
+ dependencies:
+ minimatch "^3.0.4"
+
filesize@^3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@@ -5528,6 +5552,16 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+jake@^10.6.1:
+ version "10.8.2"
+ resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
+ integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==
+ dependencies:
+ async "0.9.x"
+ chalk "^2.4.2"
+ filelist "^1.0.1"
+ minimatch "^3.0.4"
+
jest-worker@^25.4.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.5.0.tgz#2611d071b79cea0f43ee57a3d118593ac1547db1"