add blog
This commit is contained in:
1 parent
36338ed0d7
commit
7fe8f5d154
96 files changed
+21326
No files matched your search
@@ -0,0 +1,57 @@
|
||||
@import "./spacing.scss";
|
||||
|
||||
p:not([class*="title"]) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.noscroll {
|
||||
overflow: -moz-scrollbars-none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-post {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.hanzi-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
font-size: 80px;
|
||||
margin-bottom: 10px;
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.speak {
|
||||
background: none !important;
|
||||
border: none;
|
||||
padding: 0 !important;
|
||||
cursor: pointer;
|
||||
font-family: arial, sans-serif;
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
details {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.d-inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
speak(s: string, lang?: string): void
|
||||
}
|
||||
}
|
||||
|
||||
export function speak (s: string, lang?: string) {
|
||||
lang = lang || 'zh-CN'
|
||||
|
||||
if (process.client) {
|
||||
let trueLang = ''
|
||||
|
||||
const voices = speechSynthesis.getVoices()
|
||||
const [la1, la2] = lang.split(/-_/)
|
||||
if (la2) {
|
||||
const langRegex = new RegExp(`${la1}[-_]${la2}`, 'i')
|
||||
const matchedLang = voices.filter((v) => langRegex.test(v.lang))
|
||||
if (matchedLang.length > 0) {
|
||||
trueLang = matchedLang.sort((v1, v2) => v1.localService
|
||||
? -1 : v2.localService ? 1 : 0)[0].lang
|
||||
}
|
||||
}
|
||||
if (!trueLang) {
|
||||
const langRegex = new RegExp(`^${la1}`, 'i')
|
||||
const matchedLang = voices.filter((v) => langRegex.test(v.lang))
|
||||
if (matchedLang.length > 0) {
|
||||
trueLang = matchedLang.sort((v1, v2) => v1.localService
|
||||
? -1 : v2.localService ? 1 : 0)[0].lang
|
||||
}
|
||||
}
|
||||
|
||||
if (trueLang) {
|
||||
const u = new SpeechSynthesisUtterance(s)
|
||||
u.lang = trueLang
|
||||
speechSynthesis.speak(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
window.speak = speak
|
||||
|
||||
document.body.addEventListener('keydown', (evt) => {
|
||||
if (evt.code === 'KeyS') {
|
||||
const s = getSelection()
|
||||
if (s) {
|
||||
speak(s.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import qs from 'querystring'
|
||||
import showdown from 'showdown'
|
||||
import { IHyperPugFilters } from 'hyperpug'
|
||||
import h from 'hyperscript'
|
||||
import { createIndentedFilter } from 'indented-filter'
|
||||
// @ts-ignore
|
||||
import scopeCss from 'scope-css'
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
export default class MakeHtml {
|
||||
pugExtensions: IHyperPugFilters
|
||||
mdExtensions: Record<string, showdown.ShowdownExtension>
|
||||
mdConverter: showdown.Converter
|
||||
|
||||
constructor () {
|
||||
this.pugExtensions = {}
|
||||
this.mdExtensions = {
|
||||
simpleTable: {
|
||||
type: 'lang',
|
||||
filter (text) {
|
||||
const rowRegex = /(?:(?:^|\r?\n)(?:\| )?(?:(?:.* \| )+.+)*(?:.* \| )+.+(?: \|)?(?:$|\r?\n))+/m
|
||||
text = text.replace(rowRegex, (p0) => {
|
||||
return h('table.table', p0.trim().split('\n').map((pi) => {
|
||||
pi = pi.trim().replace(/^|/, '').replace(/|$/, '')
|
||||
return h('tr', pi.split(' | ').map(x => x.trim()).map((qi) => {
|
||||
return h('td', qi)
|
||||
}))
|
||||
})).outerHTML
|
||||
})
|
||||
return text
|
||||
},
|
||||
},
|
||||
pug: {
|
||||
type: 'lang',
|
||||
filter: (s) => {
|
||||
return s.replace(/\/\/ nuxt\n```pug\n([^]+?)\n```/g, (p0: string, p1: string) => {
|
||||
if (process.client) {
|
||||
return require('hyperpug').compile({ filters: this.pugExtensions })(p1)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
css: {
|
||||
type: 'lang',
|
||||
filter: (s) => {
|
||||
return s.replace(/\/\/ nuxt\n```css\n([^]+?)\n```/g, (p0: string, p1: string) => {
|
||||
return h('style', {
|
||||
attrs: {
|
||||
'data-content': p1,
|
||||
},
|
||||
}).outerHTML
|
||||
})
|
||||
},
|
||||
},
|
||||
to: {
|
||||
type: 'lang',
|
||||
filter: createIndentedFilter('^^to', (s, attrs) => {
|
||||
return h('a', { href: attrs.url }, s).outerHTML
|
||||
}),
|
||||
},
|
||||
spoiler: {
|
||||
type: 'lang',
|
||||
filter: createIndentedFilter('^^spoiler', (s, attrs) => {
|
||||
return h('details', [
|
||||
...(attrs.summary ? [
|
||||
h('summary', attrs.summary),
|
||||
] : []),
|
||||
h('div', s),
|
||||
]).outerHTML
|
||||
}),
|
||||
},
|
||||
speak: {
|
||||
type: 'lang',
|
||||
filter: createIndentedFilter('^^speak', (s, attrs) => {
|
||||
return h('button.speak', {
|
||||
attrs: {
|
||||
type: 'button',
|
||||
onclick: `speak('${attrs.s || s}', ${attrs.lang || ''})`,
|
||||
},
|
||||
}, s).outerHTML
|
||||
}),
|
||||
},
|
||||
slide: {
|
||||
type: 'lang',
|
||||
filter: createIndentedFilter('^^slide', (s, attrs) => {
|
||||
return h('a', {
|
||||
href: `https://patarapolw.github.io/reveal-md/reveal/?${qs.stringify({
|
||||
q: (() => {
|
||||
let q = s
|
||||
if (attrs.github) {
|
||||
q = `https://raw.githubusercontent.com/${attrs.github}/master/${s}`
|
||||
}
|
||||
return q
|
||||
})(),
|
||||
})}`,
|
||||
}, s).outerHTML
|
||||
}),
|
||||
},
|
||||
}
|
||||
this.mdConverter = new showdown.Converter()
|
||||
this.mdConverter.setFlavor('github')
|
||||
this.pugExtensions.markdown = (s) => {
|
||||
return this.mdConverter.makeHtml(s)
|
||||
}
|
||||
Object.entries(this.mdExtensions).forEach(([k, ext]) => this.mdConverter.addExtension(ext, k))
|
||||
}
|
||||
|
||||
parse (s: string) {
|
||||
const html = this.mdConverter.makeHtml(s)
|
||||
return this.finalize(html)
|
||||
}
|
||||
|
||||
mdConvert = this.parse
|
||||
pugConvert (s: string) {
|
||||
const html = process.client ? require('hyperpug').compile({ filters: this.pugExtensions })(s) : ''
|
||||
return this.finalize(html)
|
||||
}
|
||||
|
||||
finalize (s: string) {
|
||||
const $ = cheerio.load(s)
|
||||
$('style').each((_, el) => {
|
||||
const $el = $(el)
|
||||
const content = $el.attr('data-content')
|
||||
if (content) {
|
||||
$el.html(scopeCss(content, '.make-html-scoped'))
|
||||
}
|
||||
})
|
||||
|
||||
return $.html()
|
||||
}
|
||||
}
|
||||
|
||||
export const makeHtml = new MakeHtml()
|
||||
@@ -0,0 +1,51 @@
|
||||
$sizeUnit: rem;
|
||||
$marginKey: "m";
|
||||
$paddingKey: "p";
|
||||
$separator: "-";
|
||||
$sizes: (("0", 0), ("xxs", 0.125), ("xs", 0.25), ("sm", 0.5), ("md", 1), ("lg", 2), ("xl", 4), ("xxl", 8));
|
||||
$positions: (("t", "top"), ("r", "right"), ("b", "bottom"), ("l", "left"), ("x", "x"), ("y", "y"), ("a", "around"));
|
||||
|
||||
@function sizeValue($key, $value) {
|
||||
@return if($key == "0", 0, $value + $sizeUnit);
|
||||
}
|
||||
|
||||
@each $size in $sizes {
|
||||
$sizeKey: nth($size, 1);
|
||||
$sizeValue: nth($size, 2);
|
||||
.#{$marginKey}#{$separator}#{$sizeKey} {
|
||||
margin: sizeValue($sizeKey, $sizeValue);
|
||||
}
|
||||
.#{$paddingKey}#{$separator}#{$sizeKey} {
|
||||
padding: sizeValue($sizeKey, $sizeValue);
|
||||
}
|
||||
@each $position in $positions {
|
||||
$posKey: nth($position, 1);
|
||||
$posValue: nth($position, 2);
|
||||
.#{$marginKey}#{$posKey}#{$separator}#{$sizeKey} {
|
||||
@if $posValue == "x" {
|
||||
margin-left: sizeValue($sizeKey, $sizeValue);
|
||||
margin-right: sizeValue($sizeKey, $sizeValue);
|
||||
} @else if $posValue == "y" {
|
||||
margin-top: sizeValue($sizeKey, $sizeValue);
|
||||
margin-bottom: sizeValue($sizeKey, $sizeValue);
|
||||
} @else if $posValue == "around" {
|
||||
margin: sizeValue($sizeKey, $sizeValue);
|
||||
} @else {
|
||||
margin-#{$posValue}: sizeValue($sizeKey, $sizeValue);
|
||||
}
|
||||
}
|
||||
.#{$paddingKey}#{$posKey}#{$separator}#{$sizeKey} {
|
||||
@if $posValue == "x" {
|
||||
padding-left: sizeValue($sizeKey, $sizeValue);
|
||||
padding-right: sizeValue($sizeKey, $sizeValue);
|
||||
} @else if $posValue == "y" {
|
||||
padding-top: sizeValue($sizeKey, $sizeValue);
|
||||
padding-bottom: sizeValue($sizeKey, $sizeValue);
|
||||
} @else if $posValue == "around" {
|
||||
padding: sizeValue($sizeKey, $sizeValue);
|
||||
} @else {
|
||||
padding-#{$posValue}: sizeValue($sizeKey, $sizeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import QParser from 'q2filter'
|
||||
|
||||
export function escapeRegExp (s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function normalizeArray<T> (it: T | T[]): T | undefined {
|
||||
if (Array.isArray(it)) {
|
||||
return it[0]
|
||||
}
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
export function searchPosts (options: {
|
||||
q?: any
|
||||
isBlog?: boolean
|
||||
current?: boolean
|
||||
posts: any[]
|
||||
}) {
|
||||
let posts: any[] = options.posts
|
||||
|
||||
if (options.isBlog) {
|
||||
posts = posts.filter((p) => p.path.startsWith('blog/'))
|
||||
}
|
||||
|
||||
if (options.current) {
|
||||
const now = new Date().toISOString()
|
||||
posts = posts.filter((h) => h.date && h.date <= now)
|
||||
}
|
||||
|
||||
const parser = new QParser(options.q || {}, {
|
||||
isString: ['title', 'author', 'tag'],
|
||||
isDate: ['date'],
|
||||
filters: {
|
||||
'is:reversed': (items) => {
|
||||
return items.reverse()
|
||||
},
|
||||
},
|
||||
sortBy: {
|
||||
key: 'date',
|
||||
desc: true,
|
||||
},
|
||||
})
|
||||
|
||||
return parser.parse(posts)
|
||||
}
|
||||
|
||||
export async function apiHash (u: string, h: string): Promise<string> {
|
||||
if (typeof location !== 'undefined') {
|
||||
const url = new URL(u, location.origin)
|
||||
url.searchParams.set('h', h)
|
||||
return fetch(url.href, {
|
||||
mode: 'no-cors',
|
||||
}).then((r) => r.text())
|
||||
} else {
|
||||
if (/\.json$/i.test(u)) {
|
||||
const req = (require as any).context('@/static', true, /\.json$/i)
|
||||
return JSON.stringify(req(`.${u}`))
|
||||
} else {
|
||||
const req = (require as any).context('@/static', true, /\.(html?|md|txt|ya?ml)$/i)
|
||||
return req(`.${u}`).default
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user