remove pug
This commit is contained in:
1 parent
e397c9ae1e
commit
68840c9c60
49 files changed
+1421
-806
No files matched your search
@@ -15,5 +15,6 @@ module.exports = {
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'require-await': 0,
|
||||
'vue/no-v-html': 0,
|
||||
},
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
interface IPostTeaser {
|
||||
title: string
|
||||
path: string
|
||||
date?: string
|
||||
excerpt: string
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
class="NuxtLogo"
|
||||
width="245"
|
||||
height="180"
|
||||
viewBox="0 0 452 342"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M139 330l-1-2c-2-4-2-8-1-13H29L189 31l67 121 22-16-67-121c-1-2-9-14-22-14-6 0-15 2-22 15L5 303c-1 3-8 16-2 27 4 6 10 12 24 12h136c-14 0-21-6-24-12z"
|
||||
fill="#00C58E"
|
||||
/>
|
||||
<path
|
||||
d="M447 304L317 70c-2-2-9-15-22-15-6 0-15 3-22 15l-17 28v54l39-67 129 230h-49a23 23 0 0 1-2 14l-1 1c-6 11-21 12-23 12h76c3 0 17-1 24-12 3-5 5-14-2-26z"
|
||||
fill="#108775"
|
||||
/>
|
||||
<path
|
||||
d="M376 330v-1l1-2c1-4 2-8 1-12l-4-12-102-178-15-27h-1l-15 27-102 178-4 12a24 24 0 0 0 2 15c4 6 10 12 24 12h190c3 0 18-1 25-12zM256 152l93 163H163l93-163z"
|
||||
fill="#2F495E"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.NuxtLogo {
|
||||
animation: 1s appear;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<article>
|
||||
<post-teaser v-for="p in posts" :key="p.path" :post="p"></post-teaser>
|
||||
|
||||
<nav v-if="count > 5">
|
||||
<ul>
|
||||
<li v-if="page > 2">
|
||||
<nuxt-link :to="makePageUrl(1)" aria-label="page 1"> < </nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page > 1">
|
||||
<nuxt-link :to="makePageUrl(1)" aria-label="page 1">1</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page > 4">
|
||||
<span> … </span>
|
||||
</li>
|
||||
|
||||
<li v-if="page > 3">
|
||||
<nuxt-link
|
||||
:to="makePageUrl(page - 1)"
|
||||
:aria-label="`page ${page - 1}`"
|
||||
>
|
||||
{{ page - 1 }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span>
|
||||
{{ page }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total - 2">
|
||||
<nuxt-link
|
||||
:to="makePageUrl(page + 1)"
|
||||
:aria-label="`page ${page + 1}`"
|
||||
>
|
||||
{{ page + 1 }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total - 3">
|
||||
<span> … </span>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total">
|
||||
<nuxt-link :to="makePageUrl(total)" :aria-label="`page ${total}`">
|
||||
{{ total }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total - 1">
|
||||
<nuxt-link
|
||||
:to="makePageUrl(page + 1)"
|
||||
:aria-label="`page ${page + 1}`"
|
||||
>
|
||||
>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'nuxt-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class PostQuery extends Vue {
|
||||
@Prop({ required: true }) posts!: IPostTeaser[]
|
||||
@Prop({ required: true }) count!: number
|
||||
|
||||
get page() {
|
||||
return parseInt((this.$route.query.page as string) || '1')
|
||||
}
|
||||
|
||||
get total() {
|
||||
return Math.ceil(this.count / 5)
|
||||
}
|
||||
|
||||
makePageUrl(page: number) {
|
||||
return this.$router.resolve({
|
||||
path: this.$route.path,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
page: page.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<section>
|
||||
<h2>
|
||||
<nuxt-link :to="'post/' + post.path">
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
</h2>
|
||||
<small>
|
||||
<span>
|
||||
by
|
||||
<a
|
||||
v-if="theme.author.url"
|
||||
:href="theme.author.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ theme.author.name }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ theme.author.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="post.date" style="float: right">
|
||||
{{ post.date | formatDate }}
|
||||
</span>
|
||||
</small>
|
||||
|
||||
<article v-html="post.excerpt"></article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'nuxt-property-decorator'
|
||||
import { THEME } from '~/assets/global'
|
||||
|
||||
@Component
|
||||
export default class PostTeaser extends Vue {
|
||||
@Prop() post!: IPostTeaser
|
||||
|
||||
theme = THEME
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
article {
|
||||
margin-top: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +0,0 @@
|
||||
# COMPONENTS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
The components directory contains your Vue.js Components.
|
||||
|
||||
_Nuxt.js doesn't supercharge these components._
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 v-if="error.statusCode === 404">Page not found</h1>
|
||||
<h1 v-else>An error occurred</h1>
|
||||
<NuxtLink to="/">Home page</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['error'],
|
||||
layout: 'error', // you can set a custom layout for the error page
|
||||
}
|
||||
</script>
|
||||
@@ -27,13 +27,24 @@ const config = async (): Promise<NuxtConfig> => {
|
||||
: 'https://unpkg.com/awsm.css/dist/awsm.min.css',
|
||||
},
|
||||
],
|
||||
script: [
|
||||
{
|
||||
src:
|
||||
'https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js',
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
type: 'module',
|
||||
src: '/webcomponents.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Global CSS (https://go.nuxtjs.dev/config-css)
|
||||
css: [],
|
||||
|
||||
// Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
|
||||
plugins: [],
|
||||
plugins: ['~/plugins/format.ts'],
|
||||
|
||||
// Auto import components (https://go.nuxtjs.dev/config-components)
|
||||
components: true,
|
||||
@@ -58,6 +69,7 @@ const config = async (): Promise<NuxtConfig> => {
|
||||
env: {
|
||||
THEME: JSON.stringify(theme),
|
||||
},
|
||||
serverMiddleware: [{ path: '/api', handler: '~/server/index.ts' }],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"@nuxtjs/eslint-module": "^2.0.0",
|
||||
"@types/better-sqlite3": "^5.4.1",
|
||||
"@types/cheerio": "^0.22.23",
|
||||
"@types/ejs": "^3.0.5",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"@types/showdown": "^1.9.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"ejs": "^3.1.5",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-plugin-nuxt": "^1.0.0",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<p v-if="$fetchState.pending"></p>
|
||||
<p v-else-if="$fetchState.error">An error occurred {{ $fetchState.error }}</p>
|
||||
<post-query v-else :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<TagPage>({
|
||||
async fetch() {
|
||||
const { result, count } = await this.$axios.$get('/q', {
|
||||
params: {
|
||||
q: this.$route.query.q,
|
||||
page: this.$route.query.page,
|
||||
tag: this.$route.params.tag,
|
||||
},
|
||||
})
|
||||
|
||||
this.posts = result
|
||||
this.count = count
|
||||
},
|
||||
})
|
||||
export default class TagPage extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<p v-if="$fetchState.pending"></p>
|
||||
<p v-else-if="$fetchState.error">
|
||||
An error occurred {{ $fetchState.error.message }}
|
||||
</p>
|
||||
<p v-else-if="!posts.length">No posts found.</p>
|
||||
<post-query v-else :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<BlogPage>({
|
||||
async fetch() {
|
||||
const { result, count } = await this.$axios.$get('/api/q', {
|
||||
params: {
|
||||
q: this.$route.query.q,
|
||||
page: this.$route.query.page,
|
||||
},
|
||||
})
|
||||
|
||||
this.posts = result
|
||||
this.count = count
|
||||
},
|
||||
})
|
||||
export default class BlogPage extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,30 +1,9 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="links">
|
||||
<a
|
||||
href="https://nuxtjs.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="button--green"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/nuxt/nuxt.js"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="button--grey"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({})
|
||||
export default Vue.extend({
|
||||
middleware: (ctx) => {
|
||||
ctx.redirect('/blog')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
# PLUGINS
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
|
||||
@@ -0,0 +1,12 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
Vue.filter('formatDate', (s: string) => {
|
||||
return s
|
||||
? new Date(s).toLocaleDateString([], {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
})
|
||||
: ''
|
||||
})
|
||||
@@ -14,12 +14,6 @@ export const CONTENT_PATH = path.resolve(
|
||||
)
|
||||
|
||||
export const srcPath = (
|
||||
root: '_post' | '_media' | typeof THEME_FILENAME,
|
||||
root: '_posts' | '_media' | typeof THEME_FILENAME,
|
||||
...ps: string[]
|
||||
): string => path.join(CONTENT_PATH, root, ...ps)
|
||||
|
||||
export const builtPath = (...ps: string[]): string =>
|
||||
path.join(__dirname, '../built', ...ps)
|
||||
|
||||
export const dstMediaPath = (...ps: string[]): string =>
|
||||
path.join(__dirname, '../static/media', ...ps)
|
||||
@@ -2,6 +2,7 @@ import crypto from 'crypto'
|
||||
|
||||
import scopeCss from '@patarapolw/scope-css'
|
||||
import cheerio from 'cheerio'
|
||||
import ejs from 'ejs'
|
||||
import jieba from 'nodejieba'
|
||||
import showdown from 'showdown'
|
||||
|
||||
@@ -16,7 +17,7 @@ export class MakeHTML {
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
tasklists: true,
|
||||
simpleLineBreaks: true,
|
||||
// simpleLineBreaks: true,
|
||||
openLinksInNewWindow: true,
|
||||
backslashEscapesHTMLTags: true,
|
||||
emoji: true,
|
||||
@@ -24,7 +25,7 @@ export class MakeHTML {
|
||||
metadata: true,
|
||||
})
|
||||
|
||||
constructor(private slug: string) {
|
||||
constructor() {
|
||||
/**
|
||||
* ! For some reasons default underscore italic fails to render...
|
||||
*/
|
||||
@@ -38,18 +39,24 @@ export class MakeHTML {
|
||||
)
|
||||
}
|
||||
|
||||
parse(md: string): string {
|
||||
async parse(
|
||||
filename: string
|
||||
): Promise<{
|
||||
$: cheerio.Cheerio
|
||||
html(): string
|
||||
clean(): string
|
||||
}> {
|
||||
const id =
|
||||
'md-' +
|
||||
crypto
|
||||
.createHash('sha256')
|
||||
.update(this.slug || md)
|
||||
.update(filename)
|
||||
.digest()
|
||||
.toString('hex')
|
||||
.substr(8)
|
||||
|
||||
const $div = this.$(`<div id="${id}">`)
|
||||
$div.html(this.mdConverter.makeHtml(md))
|
||||
$div.html(await this.getHTML(filename))
|
||||
$div.find('style').each((_, el) => {
|
||||
const $el = this.$(el)
|
||||
const html = $el.html() || ''
|
||||
@@ -58,14 +65,34 @@ export class MakeHTML {
|
||||
}
|
||||
})
|
||||
|
||||
return this.$('<div>').append($div).html() || ''
|
||||
return {
|
||||
$: $div,
|
||||
html() {
|
||||
return $div.html() || ''
|
||||
},
|
||||
clean() {
|
||||
return jieba.cutForSearch($div.text() || '').join(' ')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
clean(md: string): string {
|
||||
const $div = this.$('<div>')
|
||||
$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.$('<div>')
|
||||
.html(
|
||||
/* html */ `
|
||||
<div style="padding-top:56.25%">
|
||||
<iframe src="https://www.youtube.com/embed/${id}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
style="width:100%; height:100%"></iframe>
|
||||
</div>`
|
||||
)
|
||||
.html(),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(/<!-- excerpt(?:_separator)? -->/)[0]
|
||||
)
|
||||
const contentHtml = makeHtml.parse(markdown)
|
||||
const r = await makeHtml.parse(srcPath('_posts', f))
|
||||
const contentHtml = r.html()
|
||||
const excerptHtml = contentHtml.split(
|
||||
/<!-- excerpt(?:_separator)? -->/
|
||||
)[0]
|
||||
|
||||
db.insert({
|
||||
...header,
|
||||
path,
|
||||
content,
|
||||
excerptHtml,
|
||||
contentHtml,
|
||||
db.insert({
|
||||
...header,
|
||||
path,
|
||||
content: r.clean(),
|
||||
excerptHtml,
|
||||
contentHtml,
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
+151
-11
@@ -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<typeof zPost>
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user