@ -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" | |||||
} |