@ -0,0 +1 @@ | |||
/lib/ |
@ -0,0 +1,54 @@ | |||
--- | |||
meanings: | |||
- meaning: | |||
- To Disconnect Something | |||
- To Remove Something | |||
- to take off something | |||
examples: | |||
- ja: 眼鏡を外す | |||
en: take off one's glasses | |||
- ja: 上着のボタンを外す | |||
en: unbutton [undo the buttons of] one's coat | |||
- ja: 犬の鎖を外す | |||
en: unchain a dog | |||
- ja: 箱のふたを外した | |||
en: I removed the lid from the box. | |||
- ja: ドアの鍵かぎ[留め金]を外しなさい | |||
en: Unlock [Unlatch] the door. | |||
- ja: 彼は先発メンバーから外された | |||
en: He was removed [dropped/scratched] from the starting lineup. | |||
- meaning: | |||
- to leave the room | |||
- to leave the seat | |||
- to leave | |||
skip: true | |||
examples: | |||
- ja: 席を外す | |||
en: leave one's seat | |||
- ja: 彼はいつの間にか席を外していた | |||
en: He had slipped out of the room unnoticed. | |||
- ja: 彼はただいま席を外しております | |||
en: 〔電話などで〕He is not at his desk just now. | |||
- meaning: | |||
- to elude | |||
- to dodge | |||
- to evade | |||
examples: | |||
- ja: 相手の打撃を巧みに外した | |||
en: He skillfully dodged his opponent's blow. | |||
- ja: 彼女は私の質問を外した | |||
en: She evaded my question. | |||
- ja: 投手が次の一球を外した | |||
en: The pitcher wasted the next pitch. | |||
- ja: 投手はゆるい球で打者のタイミングを外した | |||
en: The pitcher threw the batter's timing off [upset the batter's timing] with a slow pitch. | |||
- meaning: | |||
- To Miss Something | |||
- to miss the mark | |||
examples: | |||
- ja: 機会[的]を外す | |||
en: miss a chance [the mark] | |||
- meaning: | |||
- To Exclude Something | |||
- to exclude someone | |||
--- |
@ -0,0 +1,30 @@ | |||
{ | |||
"name": "@wk-extra/server", | |||
"private": true, | |||
"version": "1.0.0", | |||
"main": "lib/index.js", | |||
"license": "MIT", | |||
"scripts": { | |||
"build:server": "tsc -P src/tsconfig.json", | |||
"dev": "yarn tsmon src/index.ts", | |||
"ts": "ts-node -r tsconfig-paths/register", | |||
"tsmon": "NODE_ENV=development ts-node-dev -r tsconfig-paths/register" | |||
}, | |||
"dependencies": { | |||
"@fastify/cors": "^7.0.0", | |||
"fastify": "^3.29.0", | |||
"gray-matter": "^4.0.3", | |||
"js-yaml": "^4.1.0", | |||
"jsonschema-definer": "^1.3.2", | |||
"sanitize-filename": "^1.6.3" | |||
}, | |||
"devDependencies": { | |||
"@types/js-yaml": "^4.0.5", | |||
"fast-glob": "^3.2.11", | |||
"pino-pretty": "^7.6.1", | |||
"ts-node": "^10.7.0", | |||
"ts-node-dev": "^1.1.8", | |||
"tsconfig-paths": "^4.0.0", | |||
"typescript": "^4.6.4" | |||
} | |||
} |
@ -0,0 +1,30 @@ | |||
import fastifyCors from '@fastify/cors'; | |||
import fastify from 'fastify'; | |||
import kanjiRouter from './router/kanji'; | |||
import vocabularyRouter from './router/vocabulary'; | |||
import { isDev } from './shared'; | |||
async function main() { | |||
const port = parseInt(process.env['PORT']!) || 13358; | |||
process.env['PORT'] = port.toString(); | |||
const app = fastify({ | |||
logger: { | |||
prettyPrint: isDev, | |||
}, | |||
}); | |||
app.register(fastifyCors); | |||
app.register(kanjiRouter, { | |||
prefix: '/api/kanji', | |||
}); | |||
app.register(vocabularyRouter, { | |||
prefix: '/api/vocabulary', | |||
}); | |||
await app.listen(port, isDev ? '' : '0.0.0.0'); | |||
} | |||
main(); |
@ -0,0 +1,98 @@ | |||
import glob from 'fast-glob'; | |||
import { FastifyPluginAsync } from 'fastify'; | |||
import yaml from 'js-yaml'; | |||
import S from 'jsonschema-definer'; | |||
import sanitize from 'sanitize-filename'; | |||
import { oItem, readYAML } from '../shared'; | |||
export const kanjiRouter: FastifyPluginAsync = async (f) => { | |||
f.get<{ | |||
Params: { | |||
entry: string; | |||
}; | |||
}>(':entry.md', async (req) => { | |||
const { entry } = req.params; | |||
const m = getEntry(entry); | |||
if (!m) { | |||
throw { statusCode: 404 }; | |||
} | |||
const { data, content } = m; | |||
if (content.trim()) return content; | |||
let md = ''; | |||
if (data.meanings) { | |||
md += data.meanings | |||
.map((v, i) => { | |||
const out = [`${i + 1}. ${v.meaning.join('; ')}`]; | |||
if (v.examples) { | |||
out.push( | |||
...v.examples.map( | |||
(ex) => ' - ' + ex.ja + (ex.en ? `\n - ` + ex.en : ''), | |||
), | |||
); | |||
} | |||
return out.join('\n'); | |||
}) | |||
.join('\n'); | |||
} | |||
return ( | |||
md || | |||
yaml | |||
.dump(data) | |||
.split('\n') | |||
.map((ln) => (ln ? ' ' + ln : ln)) | |||
.join('\n') | |||
); | |||
}); | |||
f.get<{ | |||
Params: { | |||
entry: string; | |||
}; | |||
}>(':entry', async (req): Promise<typeof sEntry.type> => { | |||
const { entry } = req.params; | |||
const m = getEntry(entry); | |||
if (!m) { | |||
throw { statusCode: 404 }; | |||
} | |||
return m.data; | |||
}); | |||
}; | |||
export default kanjiRouter; | |||
const sEntry = S.shape({ | |||
readings: S.list( | |||
S.shape({ | |||
reading: S.string(), | |||
type: S.string(), | |||
...oItem, | |||
}).additionalProperties(true), | |||
).optional(), | |||
meanings: S.list( | |||
S.shape({ | |||
meaning: S.list(S.string()), | |||
...oItem, | |||
}).additionalProperties(true), | |||
).optional(), | |||
}).additionalProperties(true); | |||
function getEntry(entry: string) { | |||
const filepath = glob.sync(`assets/kanji/**/${sanitize(entry)}.md`).sort()[0]; | |||
if (!filepath) return null; | |||
const { data, content } = readYAML(filepath); | |||
return { | |||
data: sEntry.ensure(data), | |||
content, | |||
}; | |||
} |
@ -0,0 +1,100 @@ | |||
import glob from 'fast-glob'; | |||
import { FastifyPluginAsync } from 'fastify'; | |||
import yaml from 'js-yaml'; | |||
import S from 'jsonschema-definer'; | |||
import sanitize from 'sanitize-filename'; | |||
import { oItem, readYAML } from '../shared'; | |||
export const vocabularyRouter: FastifyPluginAsync = async (f) => { | |||
f.get<{ | |||
Params: { | |||
entry: string; | |||
}; | |||
}>('/:entry.md', async (req) => { | |||
const { entry } = req.params; | |||
const m = getEntry(entry); | |||
if (!m) { | |||
throw { statusCode: 404 }; | |||
} | |||
const { data, content } = m; | |||
if (content.trim()) return content; | |||
let md = ''; | |||
if (data.meanings) { | |||
md += data.meanings | |||
.map((v, i) => { | |||
const out = [`${i + 1}. ${v.meaning.join('; ')}`]; | |||
if (v.examples) { | |||
out.push( | |||
...v.examples.map( | |||
(ex) => ' - ' + ex.ja + (ex.en ? `\n - ` + ex.en : ''), | |||
), | |||
); | |||
} | |||
return out.join('\n'); | |||
}) | |||
.join('\n'); | |||
} | |||
return ( | |||
md || | |||
yaml | |||
.dump(data) | |||
.split('\n') | |||
.map((ln) => (ln ? ' ' + ln : ln)) | |||
.join('\n') | |||
); | |||
}); | |||
f.get<{ | |||
Params: { | |||
entry: string; | |||
}; | |||
}>('/:entry', async (req): Promise<typeof sEntry.type> => { | |||
const { entry } = req.params; | |||
const m = getEntry(entry); | |||
if (!m) { | |||
throw { statusCode: 404 }; | |||
} | |||
return m.data; | |||
}); | |||
}; | |||
export default vocabularyRouter; | |||
const sEntry = S.shape({ | |||
readings: S.list( | |||
S.shape({ | |||
reading: S.list(S.string()), | |||
...oItem, | |||
}).additionalProperties(true), | |||
).optional(), | |||
meanings: S.list( | |||
S.shape({ | |||
meaning: S.list(S.string()), | |||
...oItem, | |||
}).additionalProperties(true), | |||
).optional(), | |||
}).additionalProperties(true); | |||
function getEntry(entry: string) { | |||
console.log(entry); | |||
const filepath = glob | |||
.sync(`assets/vocabulary/**/${sanitize(entry)}.md`) | |||
.sort()[0]; | |||
if (!filepath) return null; | |||
const { data, content } = readYAML(filepath); | |||
return { | |||
data: sEntry.ensure(data), | |||
content, | |||
}; | |||
} |
@ -0,0 +1,32 @@ | |||
import { readFileSync } from 'fs'; | |||
import matter from 'gray-matter'; | |||
import yaml from 'js-yaml'; | |||
import S from 'jsonschema-definer'; | |||
export const isDev = process.env['NODE_ENV'] === 'development'; | |||
export function readYAML(filepath: string) { | |||
return matter(readFileSync(filepath, 'utf-8'), { | |||
engines: { | |||
yaml: { | |||
parse: (s) => | |||
s | |||
? (yaml.load(s, { | |||
schema: yaml.JSON_SCHEMA, | |||
}) as Record<string, any>) | |||
: {}, | |||
}, | |||
}, | |||
}); | |||
} | |||
export const oItem = { | |||
skip: S.boolean().optional(), | |||
examples: S.list( | |||
S.shape({ | |||
ja: S.string(), | |||
en: S.string().optional(), | |||
}).additionalProperties(true), | |||
).optional(), | |||
}; |
@ -0,0 +1,7 @@ | |||
{ | |||
"extends": "../tsconfig.json", | |||
"compilerOptions": { | |||
"rootDir": ".", | |||
"outDir": "../lib" | |||
} | |||
} |
@ -0,0 +1,3 @@ | |||
{ | |||
"extends": "../../tsconfig.json" | |||
} |