æçµæŽæ°: 2026-08-30ïŒå¯Ÿè±¡ããŒãžã§ã³: makeitaquote@12.0.0 ãŸã§ã®èšèšå€æã环ç©ããèšé²
ãªããžããª: https://github.com/otnc/makeitaquote ïŒ ãªããžããªæ§æã®ããŒã¹: oto-lab/npm-biome-ts
ãã®ææžã«ã€ããŠ: §1ã§13 㯠2026-07-31 ã«æžãã v9 æç¹ã®è©³çްèšèšæžã§ãåœæã®å€æããã®ãŸãŸæ®ããŠããïŒv8 ãšã®åŸæ¹äºæãæããªãããšãã § 1.3 ã®æ±ºå®ãå«ããäºå®ãšããŠéå»ã«ç¢ºå®ããå 容ã ããïŒãv9 以éã®å€æŽã¯ §14 以éã«çããšã®ç¯ãšããŠè¿œèšããŠãããæ¬äœã®èšè¿°ãšççŸããå Žåã¯çªå·ã®å€§ããç¯ãåªå ãããä»ã®å§¿ãç¥ãããã ããªãæ¬äœã¯èªã¿é£ã°ããäžã®äžèЧãã該åœããç¯ã«çŽæ¥é£ãã§ããã
ç ç¯ äœãå€ãã£ãã v9.1 §14 瞊é·ã¬ã€ã¢ãŠãã»å転ã»å€ªå v9.2 §15 ãã©ã³ãå解決㮠API çµç±åãMisskey ã«ã¹ã¿ã çµµæå v9.3 §16 æãè¿ãã®å®ãã°ä¿®æ£ãè²ã·ã¹ãã ããµã€ãºãå圢ãã©ãŒã«ãã㯠v10 â ãã©ã³ãïŒTwemoji ãã£ãã·ã¥ããããžã§ã¯ãããŒã«ã«ã«ïŒREADME/MIGRATING åç §ãæ¬æžã«ç¯ãªãïŒ v11 â äŒè©±ã»ãã€ãŒãåã蟌ã¿ãªã©ïŒREADME åç §ãæ¬æžã«ç¯ãªãïŒ v12 §19 makeitaquote/apiãµããã¹ã廿¢ã@makeitaquote/voidsãžåé¢v12 §20 markdownãªãã·ã§ã³è¿œå â 倪å/æäœ/äžç·/åãæ¶ãç·ãé€å»ããæç»ã§ããããã«v12 §21 watermark ã®ç»å察å¿ã MiQConversationãMiQChainïŒè¿ä¿¡/åŒçšãã§ãŒã³ç»åïŒã«çœ®ãæãv10ã»v11 ã§æ¬æžã«ç¯ããªãã®ã¯ãåœæããã«è¿œèšããéçšããŸã ç¡ãã£ããããREADMEã»MIGRATING.md ã®èšèŒãäžæ¬¡æ å ±ã«ãªãã
çŸè¡ v8 ã¯ãVoids API ã« POST ã㊠URL ã Buffer ãåãåãã ããã®èã Wrapper ã§ããã以äžã®åé¡ãæ±ããŠããã
| åé¡ | å 容 |
|---|---|
| å€éš API å šé¢äŸå | api.voids.top ãèœã¡ããšããã±ãŒãžãæ©èœåæ¢ãã |
| ãšã³ããã€ã³ãã®æå³ã¥ãã®èª€ã | generate() / generateBeta() ããæ¬çªãšãã©ãŒã«ããã¯ããšã㊠README ã«æžããŠããããå®éã«ã¯æ©èœãç°ãªã 2 ã€ã®ãšã³ããã€ã³ãã§ããïŒÂ§2.1ïŒ |
| åãšå®è£ ã®ä¹é¢ | index.d.ts ãææžãã§ãå®è£
ãšåæããä¿èšŒããªã |
| 責åã®æ··åš | Discord äŸåïŒsetFromMessageïŒã»æŽåœ¢ïŒdisplusïŒã»HTTPïŒaxiosïŒã 1 ãã¡ã€ã«ã«åå±
|
| æ¡åŒµäœå°ã®æ¬ åŠ | ç»åãµã€ãºã»é è²ã»ãã©ã³ãã API åŽã«åºå®ãããå©çšè ãäžåå¶åŸ¡ã§ããªã |
- API éäŸåã®ããŒã«ã«ç»åçæã第äžçŽã®æ©èœãšããŠå®è£
ããïŒ
makeitaquoteïŒã - Voids API äŸåã®å®è£
ã¯å¥ãµããã¹ã«éé¢ããïŒ
makeitaquote/apiïŒã - TypeScript ã§æžãçŽããåå®çŸ©ãå®è£ ããèªåçæããã
- Discord ã«ã¹ã¿ã çµµæåã»Twemojiã»æ¥æ¬èªããã¹ããæ£ããæç»ããã
- èŠãç®ïŒãµã€ãºã»é è²ã»ãã©ã³ãã»ã¬ã€ã¢ãŠãïŒãå©çšè ãå¶åŸ¡ã§ããããã«ããã
- ãã©ã³ãæªèšå®ã§ãæ¥æ¬èªãæç»ã§ããããã«ããïŒÂ§8ïŒã
- ãªããžããªæ§æã»CIã»ãªãªãŒã¹ãããŒã
oto-lab/npm-biome-tsã«æããïŒÂ§5ïŒã
- v8 API ãšã®åŸæ¹äºæïŒäžèŠãšæ±ºå®æžã¿ïŒ
- ãã©ãŠã¶ç°å¢ã§ã®åäœïŒNode.js å°çšã
@napi-rs/canvasããã€ãã£ããã€ã³ãã£ã³ã°ã®ããïŒ - åç»ã»GIF ã¢ãã¡ãŒã·ã§ã³ã®åºåïŒã¢ãã¡çµµæåã鿢ãã¬ãŒã ãšããŠæç»ããïŒ
çŸè¡ã³ãŒãã粟èªããçµæããã® 2 ã€ã¯ãåãæ©èœã®æ¬çªïŒããŒã¿ãã§ã¯ãªããè¿ããã®ãæ ¹æ¬çã«éãã
// index.js:171-172 ââ /fakequote
const response = await axios.post(API_URL, this.format, { responseType: 'json' })
return response.data.url // â JSON { url } ãè¿ã
// index.js:166-169 ââ /fakequote + returnRawImage
const response = (await axios.post(API_URL, this.format)).data
const imageBuffer = await axios.get(response.url, { responseType: 'arraybuffer' })
return Buffer.from(imageBuffer.data, 'binary') // â URL ãåã£ãŠãã âããäžåºŠâ GET
// index.js:197-198 ââ /fakequotebeta
const response = (await axios.post(BETA_API_URL, this.format, { responseType: 'arraybuffer' })).data
return Buffer.from(response) // â ç»åãã€ããªãçŽæ¥è¿ã/fakequote |
/fakequotebeta |
|
|---|---|---|
| ã¬ã¹ãã³ã¹ | application/json â { url: "..." } |
ç»åãã€ã㪠|
| ç»åã®æåš | Voids åŽã«ã¢ããããŒãããããã¹ã URL ãçºè¡ããã | ã©ãã«ãä¿åãããªããéœåºŠçæããŠè¿ãã ã |
| URL ã®ååŸ | ã§ãã | ã§ããªãïŒåççã«ïŒ |
| Buffer ååŸæã®åŸåŸ©æ° | 2 åŸåŸ©ïŒPOST â JSON â GETïŒ | 1 åŸåŸ© |
| Discord ã§ã®äœ¿ãæ¹ | URL æååããã®ãŸãŸ reply() ã§ããïŒexamples/miq-with-discordjs.md:15ïŒ |
MessageAttachment ã§ã®æ·»ä»ãå¿
èŠïŒexamples/beta-with-discordjs.md:15ïŒ |
ãã®å·®ã v9 ã® API èšèšã決ããã
- ãURL ãæ¬²ãããâ
/fakequoteããéžæè¢ããªã - ãBuffer ã ãæ¬²ãããâ
/fakequotebetaã®ã»ãã 1 åŸåŸ©ã§éãããããç»åã第äžè ã®ãµãŒããŒã«æ®ããªã
ãããã£ãŠ v8 ã®ããã«ãã¡ãœããåã§ãšã³ããã€ã³ããéžã°ãããã®ã§ã¯ãªããæ¬²ãããã®ãèšãã°é©åãªãšã³ããã€ã³ããèªåçã«éžã°ããèšèšã«ããïŒÂ§6.4ïŒãREADME ã®ãgenerate() ã䜿ããªããšã㯠generateBeta()ããšããèšè¿°ã¯èª€è§£ãæããããv9 ã§ã¯äž¡è
ã察çãªå¥æ©èœãšããŠèª¬æãçŽãã
çæãããç»åãã¶ã€ã³ãäž¡ãšã³ããã€ã³ãã§åäžãã©ããã¯ã³ãŒãããã¯å€å®ã§ããªãããã§ãŒãº 3 ã®å®è£ æã«å®åºåãæ¯èŒããå·®ãããã° README ã®å¯Ÿå¿è¡šã«æ³šèšããã
| 察象 | çç± |
|---|---|
axios |
ky ã«çœ®æïŒÂ§3.5ïŒ |
displus |
setText(text, formatText) ã® Markdown é€å»ã®ããã ãã«äœ¿ãããŠãããæŽåœ¢ã¯æ¬ããã±ãŒãžã®è²¬åã§ã¯ãªãããæ©èœããšå»æ¢ |
.github/dependabot.yml |
廿¢ãããŒã¹ãã³ãã¬ãŒãã«ååšããããã€ãã£ããã€ããªãå«ãæ¬ããã±ãŒãžã§ã¯èªå PR ã®æ€èšŒã³ã¹ãã«èŠåããªããäŸåæŽæ°ã¯æåã§è¡ã |
v12 ã§äžéšäžæžã: ãæŽåœ¢ã¯æ¬ããã±ãŒãžã®è²¬åã§ã¯ãªãããšããäžèšã®å€æã¯ãé€å»æ©èœïŒ
displusã®åœ¹å²ïŒã«ã€ããŠã®ãã®ã§ãããv12 ã®markdownãªãã·ã§ã³ã¯é€å»ã«å ããŠæç»ãšããæ°ããçš®é¡ã®æ©èœã远å ãããã®ãåœædisplusãåã£ãçç±ïŒMarkdown é€å»ã®ããã ãã®èãäŸåãå¢ãããããªãïŒã¯ä»åãèžè¥²ããŠãããæ°èŠäŸåã¯è¿œå ããŠããªããçµç·¯ã¯ §20 ãåç §ã
| é å | æ¡çš | ããŒãžã§ã³ | åºå |
|---|---|---|---|
| æç»ãšã³ãžã³ | @napi-rs/canvas |
^1.0.3 |
dependencies |
| çµµæåããŒã¹ | @twemoji/parser |
^17.0.2 |
dependencies |
| æ¥æ¬èªæ¹è¡ | budoux |
^0.8.4 |
dependencies |
| HTTP ã¯ã©ã€ã¢ã³ã | ky |
^2.0.2 |
dependencies |
| èšèª | TypeScript | ^6.0.3 |
devDependencies |
| ãã³ãã© | tsdown |
^0.22.14 |
devDependencies |
| ãã¹ã | vitest + @vitest/coverage-v8 |
^4.1.10 |
devDependencies |
| Lint / Format | @biomejs/biome |
^2.5.6 |
devDependencies |
| ã©ã³ã¿ã€ã | Node.js | >=22 |
engines |
ãã¹ãŠ 2026-07-31 æç¹ã®ææ°ãç¢ºèªæžã¿ãããŒãžã§ã³ã¯ããŒã¹ãã³ãã¬ãŒã oto-lab/npm-biome-ts ãšäžèŽããŠããã
æ¡çšçç±
-
Skia ããŒã¹ïŒChrome ãšåãæç»ãšã³ãžã³ïŒã§ãããã¹ãã¬ã³ããªã³ã°å質ãé«ãã
-
å šäž»èŠãã©ãããã©ãŒã ã®ããªãã«ããã€ããªã忢±ïŒv1.0.3 ã®
optionalDependenciesã§ç¢ºèªïŒãdarwin-x64 / darwin-arm64 / android-arm64 linux-x64-gnu / linux-x64-musl linux-arm64-gnu / linux-arm64-musl linux-arm-gnueabihf / linux-riscv64-gnu win32-x64-msvc / win32-arm64-msvcç¹ã« muslïŒAlpineïŒãã€ããªãããç¹ã node-canvas ãšã®æ±ºå®çãªå·®ãDiscord Bot ã¯
node:*-alpineã§åããããããšãå€ããnode-canvas ã ãš Cairoã»Pangoã»libjpeg çãapk addããå¿ èŠãããã -
Canvas 2D API æºæ ãªã®ã§ãã°ã©ããŒã·ã§ã³ã»ã¯ãªããã»ãã£ã«ã¿ã»è¡éããèªåã§çްããçµããã
-
掻çºã«ã¡ã³ããã³ã¹ãããŠããïŒv1.0.3 ã 2026-07-28 ãªãªãŒã¹ïŒã
äžæ¡çšã«ããéžæè¢
| åè£ | äžæ¡çšçç± |
|---|---|
canvasïŒnode-canvasïŒ |
v3.2.3 ã§ãããªãã«ã察象ãçã musl é察å¿ããã«ã倱æãæé »åºã®ãµããŒãæ¡ä»¶ã«ãªã |
skia-canvas |
æç»å質ã¯è¯ãã Alpine/musl é察å¿ãDL æ°ãäžæ¡å°ãªã |
satori + resvg-js |
ã¢ãã¿ãŒã®ã°ã¬ãŒã¹ã±ãŒã«ïŒã°ã©ããŒã·ã§ã³åæã CSS 衚çŸã®å¶çŽãåããããã©ã³ããæ¯å Buffer ã§æž¡ãå¿ èŠãããããã€ãã£ãäŸåãçµå± 2 ã€ã«ãªã |
泚æç¹
GlobalFonts.register(buffer, name)ã¯æž¡ããã Buffer ã®ã¡ã¢ãªãåç §ããïŒã³ããŒããªãïŒãç»é²ã«äœ¿ã£ã Buffer ãè§£æŸã»åå©çšããŠã¯ãªããªãïŒBrooooooklyn/canvas#1006ïŒãFontRegistryã Buffer ãžã®åç §ãä¿æãç¶ããããšã§å¯ŸåŠããã- WOFF2 ã®èªã¿èŸŒã¿å¯åŠãããŒãžã§ã³ã«ããäžå®å®ãèªåååŸãããã©ã³ã㯠TTF ã«éå®ããïŒÂ§8.2ïŒã
åè£ãšããŠæãã£ã 2 ã©ã€ãã©ãªã®ãœãŒã¹ã粟èªããçµæãäŸåãšããŠã¯æ¡çšãããã¢ã«ãŽãªãºã ãåèã«èªåå®è£ ããã
| ã©ã€ãã©ãª | ç | äŸå | å€å® |
|---|---|---|---|
node-canvas-with-twemoji-and-discord-emojiïŒflazepeïŒ |
1.2.2 (2025-02) | canvas@^3.1.0, @twemoji/parser@^15.1.1 |
åèå®è£ |
@miq4d/canvas-with-discord-content |
2.1.2 (2026-06) | canvas@^2.11.2, twemoji@^14.0.2 |
åèå®è£ |
äŸåãšããŠæ¡çšããªãçç±
- ã©ã¡ãã
require("canvas")ãå éšã§çŽæ¥åŒãã§ããïŒutils/loadTwemojiImageByUrl.js:2ïŒã@napi-rs/canvasãšäœµçšã§ãããæ¡çšãããšæç»ãšã³ãžã³ã node-canvas ã«åºå®ãããã - è¡æãè¿ãã«é察å¿ã
drawTextWithTwemoji.js:38-40ã®maxWidthã¯ctx.setTransform(scale, 0, 0, 1, 0, 0)ã«ããæšªæ¹åã®å§çž®ã§ãã£ãŠ word wrap ã§ã¯ãªããMiQ ã¯æ¬æã®æãè¿ããäžæ žæ©èœãªã®ã§ãã©ã¡ãã䜿ã£ãŠãã¬ã€ã¢ãŠãå±€ã¯èªåã§æžãããšã«ãªãã - ç»åãã£ãã·ã¥ã jsDelivr URL ã®ã¿å¯Ÿè±¡ã»äžéãªãã®
Mapãåžžé§ Bot ã§ã¡ã¢ãªãå調å¢å ããã - æç»ã«ãŒãå
ã§é次
await loadImage(url)ããŠãããçµµæå 20 åã§ 20 åŸåŸ©ã®çŽå HTTP ã«ãªãã @miq4dçã¯canvas@^2+twemoji@14ïŒ@twemoji/parserã®å身ïŒãšäŸåãå€ãã
ç¶æ¿ããèšèš
@twemoji/parserã®parse(text, { assetType })ã§ããã¹ããèµ°æ»ããæšæºçµµæåã®indicesãš CDN URL ãåŸãã- Discord ã«ã¹ã¿ã çµµæåã¯æ£èŠè¡šçŸ
/<(a)?:(\w{2,32}):(\d{17,20})>/ã§æ€åºããhttps://cdn.discordapp.com/emojis/{id}.{png|gif}ã«è§£æ±ºããã - çµµæåã¯
fontSizeåæ¹ã®drawImageãšããŠãããã¹ãã®ããŒã¹ã©ã€ã³åºæºã§é 眮ããã
远å ã§å®è£ ãããã®
- è¡æãè¿ãïŒBudouX + ã°ã©ãã§ã å¢ç + çŠåã§7.3ïŒ
- LRU ãã£ãã·ã¥ïŒäžéã»TTLã»in-flight åæµã»è² ã®ãã£ãã·ã¥ã§9.2ïŒ
- ã¢ãã¡çµµæåïŒ
<a:...>ïŒã® 1 ãã¬ãŒã ç®æç» - ååŸå€±ææã®ãã©ãŒã«ããã¯
- çµµæåååŸã®äžŠåããªãã§ãã
æ¹è¡äœçœ®ã®æ±ºå®ã«åœ¢æ
çŽ è§£æã䜿ããããšããèŠæã«å¯Ÿããkuromoji / kuroshiro ã調æ»ããçµæãbudoux ãæ¡çšããã
| åè£ | ç / æŽæ° | å±éãµã€ãº | å€å® |
|---|---|---|---|
kuromoji |
0.1.2 / 2022-06 | 41.3 MB | äžæ¡çš |
kuroshiro |
1.2.0 / 2022-06 | â | äžæ¡çš |
kuroshiro-analyzer-kuromoji |
1.1.0 / 2022-05 | â | äžæ¡çš |
kuromojinïŒkuromoji ã©ããïŒ |
3.0.1 / 2025-04 | kuromoji äŸå | äžæ¡çš |
budoux |
0.8.4 / 2026-05 | 2.6 MBïŒå šèšèªãæ¥æ¬èªã¢ãã«ã®ã¿ãªãæ°å KBïŒ | æ¡çš |
kuromoji ç³»ãæ¡ããªãçç±
- å±éãµã€ãº 41.3 MBãIPADic èŸæžãäžžããšå«ããç»åçæã©ã€ãã©ãªã®äŸåãšããŠã¯é倧ã§ã
@napi-rs/canvasã®ãã€ããªãšåããããšã€ã³ã¹ããŒã«ãµã€ãºã 50MB ãè¶ ããã - èŸæžã®ããŒããéåæãã€æ°ç§ããããBot ã®ååãªã¯ãšã¹ããç®ã«èŠããŠé ããªãã
- 2022 幎ããæŽæ°ãæ¢ãŸã£ãŠããã
kuroshiroç³»ãåæ§ã - ããããçšéã«å¯ŸããŠéå°ãå¿ èŠãªã®ã¯ãåè©ã»èªã¿ã»å圢ãã§ã¯ãªããããã§æ¹è¡ããŠããããã®äžç¹ã ãã圢æ çŽ è§£æã¯ç®çã§ã¯ãªãææ®µã§ãããããçŽæ¥çãªææ®µãååšããã
BudouX ãæ¡ãçç±
-
æ¹è¡äœçœ®ã®æ±ºå®ãã®ãã®ãç®çã« Google ãäœã£ãã©ã€ãã©ãªãChrome ã® CSS
word-break: auto-phraseïŒChrome 119+ïŒã®å®è£ ã«äœ¿ãããŠãããã®ãšåã AdaBoost ã¢ãã«ã -
åæ APIãèŸæžããŒãåŸ ã¡ããªãã
import { jaModel, Parser } from 'budoux' const parser = new Parser(jaModel) parser.parse('仿¥ã¯å€©æ°ã§ãã') // â ['仿¥ã¯', '倩æ°ã§ãã']
loadDefaultJapaneseParser()ã§ã¯ãªãParser+jaModelãçŽæ¥äœ¿ãããšã§ãHTMLProcessorãå«ãŸãªãæå°æ§æã«ãªãã -
æ¥æ¬èªã¢ãã«ã®ã¿ã import ã§ãããããå®éã«ãã³ãã«ãããã®ã¯æ°å KBã
-
äžåœèªïŒç°¡äœã»ç¹äœïŒã»ã¿ã€èªã®ã¢ãã«ã忢±ãããŠãããå°æ¥ã®å€èšèªå¯Ÿå¿ã®äœå°ãããã
-
2026-05 æŽæ°ãšçŸåœ¹ã
äœçœ®ã¥ã: BudouX ã¯ãæç¯å¢çãè¿ããã ãã§ãããæãè¿ãã¢ã«ãŽãªãºã å šäœã眮ãæãããã®ã§ã¯ãªãã§7.3 ã®åè£äœçœ®æ±ºå®ã«ãããŠãCJK ã¯ååäœã§æãããšããçŽ æŽãªèŠåã®ä»£ããã«æç¯å¢çãåªå åè£ãšããŠäœ¿ãããšããçµã¿èŸŒã¿æ¹ããããBudouX ã倱æããŠãååäœãã©ãŒã«ããã¯ã§å¿ ãæããã®ã§ãå質åäžã®äžä¹ããšããŠæ©èœããã
- äŸåãŒããFetch API ããŒã¹ã~15KBã
- ãªãã©ã€ã»ã¿ã€ã ã¢ãŠãã»ããã¯ãçµã¿èŸŒã¿ãçµµæå CDN ã®äžæç倱æã«åŒ·ãã
ky 㯠ESM-onlyïŒky@2.0.2 ã® package.json 㯠"type": "module"ãexports ã« require æ¡ä»¶ãªãïŒã
Node 22.12+ ã§ã¯ require(ESM) ãåããã22.0â22.11 ã§ã¯åããªããengines ã >=22 ã§ãã以äžããã«äŸåã§ããªããããtsdown ã® deps.alwaysBundle ã§ CJS åºåã«ãã³ãã«ããŠåã蟌ãïŒÂ§5.5ïŒãky ã¯äŸåãŒããªã®ã§å¯äœçšããªãã
setFromMessage() ã®ããã« discord.js ãžäŸåããªãã**æ§é çåïŒduck typingïŒ**ã§åããã
export interface MessageLike {
content: string
author: {
username: string
globalName?: string | null
global_name?: string | null
discriminator?: string | null
displayAvatarURL?: (options?: unknown) => string
}
member?: {
displayName?: string
nickname?: string | null
displayAvatarURL?: (options?: unknown) => string
} | null
}discord.js v13 / v14 / discord.js-selfbot-v13 ã®ãããã® Message ããã®åœ¢ãæºãããããpeerDependencies ããäžèŠããã¹ãã§ãã¢ãã¯ã容æã«ãªãã
v12 ã§å»æ¢:
makeitaquote/apiãµããã¹ã¯åé€ããå€éš API ã¯ã©ã€ã¢ã³ãã¯å¥ããã±ãŒãž@makeitaquote/voidsãžåãåºããã以äžã¯ v9ãv11 ã®èšèšèšé²ãšããŠæ®ããçµç·¯ãšçŸè¡æ§æã¯ §19 ãåç §ã
åäž npm ããã±ãŒãž + subpath exportsãnpm äžã®ããã±ãŒãžå㯠makeitaquote 1 ã€ã
require('makeitaquote') â ããŒã«ã«ç»åçæïŒAPI éäŸåïŒ
require('makeitaquote/api') â Voids API ã¯ã©ã€ã¢ã³ã
ãµããã¹å㯠voids ã§ã¯ãªã api ãšããããå€éš API ãå©ãåŽããšãã圹å²ã衚ããå°æ¥ Voids 以å€ã® API å®è£
ãè¶³ããŠãååãç Žç¶»ããªãã
makeitaquote/api 㯠ky ãã䜿ããªãã@napi-rs/canvasïŒãã€ã㪠~10MBïŒã budoux ãåŒãããããªãã
@napi-rs/canvas 㯠optionalDependencies ã§ã¯ãªãéåžžã® dependencies ã«çœ®ããæ¬äœããã v9 ã®äž»æ©èœã§ãããoptional ã«ãããšãå
¥ã£ãŠããªããç¶æ
ã«é¥ããããããã
ãã®ä»£ãã src/api/* 㯠@napi-rs/canvas ãš budoux ãäžå import ããªãããã«ãã«ãå¢çãåããmakeitaquote/api ã import ããã ãã§ã¯ãã€ãã£ãã¢ãžã¥ãŒã«ãããŒããããªãç¶æ
ãä¿èšŒããïŒbuild æ€èšŒã§æ
ä¿ã§5.8ïŒã
ããŒã¹ãã³ãã¬ãŒãã«åŸãããã¹ãã¯ãœãŒã¹ã«äœµçœ®ïŒsrc/**/*.test.tsïŒãããtest/ ãã£ã¬ã¯ããªã¯äœããªãã
makeitaquote/
âââ .editorconfig
âââ .gitattributes
âââ .gitignore
âââ biome.json
âââ package.json
âââ tsconfig.json
âââ tsdown.config.ts
âââ vitest.config.ts
âââ CONTRIBUTING.md
âââ LICENSE
âââ README.md
âââ .github/
â âââ FUNDING.yml # æ¢åãç¶æ
â âââ PULL_REQUEST_TEMPLATE.md
â âââ ISSUE_TEMPLATE/
â â âââ 1-bug-report.yml
â â âââ 2-feature-request.yml
â â âââ config.yml
â âââ workflows/
â âââ ci.yml
â âââ release.yml
â âââ ïŒdependabot.yml ã¯åé€ïŒ
âââ scripts/
â âââ check-build.js # ãã«ãææç©ã®æ€èšŒïŒÂ§5.8ïŒ
âââ src/
âââ index.ts # ã«ãŒããšã³ããªïŒre-export ã®ã¿ïŒ
âââ core/
â âââ MiQ.ts
â âââ MiQ.test.ts
â âââ quote.ts
â âââ source.ts # MessageLike â QuoteData
â âââ source.test.ts
â âââ errors.ts
â âââ types.ts
âââ theme/
â âââ presets.ts # dark / light / color
â âââ resolve.ts
â âââ resolve.test.ts
â âââ types.ts
âââ render/
â âââ pipeline.ts
â âââ pipeline.test.ts # smoke testïŒå®éã« PNG ãçæïŒ
â âââ background.ts
â âââ avatar.ts
â âââ quoteText.ts
â âââ attribution.ts
â âââ canvasFactory.ts # @napi-rs/canvas ã®å¯äžã® import å°ç¹
âââ text/
â âââ segment.ts
â âââ segment.test.ts
â âââ measure.ts
â âââ breakpoint.ts # BudouX + ã°ã©ãã§ã + çŠå
â âââ breakpoint.test.ts
â âââ wrap.ts
â âââ wrap.test.ts
â âââ fit.ts
â âââ fit.test.ts
â âââ draw.ts
âââ emoji/
â âââ resolve.ts
â âââ resolve.test.ts
â âââ loader.ts
â âââ cache.ts
âââ font/
â âââ registry.ts
â âââ autoload.ts
â âââ autoload.test.ts
â âââ diskCache.ts
â âââ sources.ts
âââ output/
â âââ encode.ts
âââ http/
â âââ client.ts
âââ util/
â âââ lru.ts
â âââ lru.test.ts
â âââ grapheme.ts
â âââ assert.ts
âââ __fixtures__/ # ãã¹ãçšã® PNG / ã¡ãã»ãŒãžã¢ãã¯
â âââ emoji-1f600.png
â âââ avatar.png
â âââ messages.ts
âââ api/
âââ index.ts # ãµããã¹ãšã³ããª
âââ client.ts # VoidsMiQ
âââ client.test.ts
âââ endpoints.ts
âââ types.ts
__fixtures__/ 㯠tsdown ã® entry ã«å«ããªããããã«ãææç©ã«å
¥ããªããfiles: ["dist"] ãªã®ã§ npm é
åžç©ã«ãå«ãŸããªãã
ããŒã¹ãã³ãã¬ãŒãã®åãã¡ã€ã«ããã®ãŸãŸæ¡çšããæ¬ããã±ãŒãžåºæã®äºæ ã«ããå·®åã®ã¿ãå ãããå·®å㯠§5.9 ã«äžèЧããã
ãã³ãã¬ãŒãã®ãã£ãŒã«ãé ã»ã¹ã¯ãªããåãèžè¥²ãããexports ã«ãµããã¹ ./api ã远å ããç¹ãäž»ãªå·®åã
2026-08-01 ã§ã®èšæ£:
"license"ã¯ãã®åŸ ISC â MIT ã«å€æŽããããå§åйããã±ãŒãžã®ã©ã€ã»ã³ã¹æ¹éã«åããããã®ã§ãv9 ç³»ããã®å€æŽã®èµ·ç¹ãäžã®ã¹ãããã·ã§ããã¯èšèšæç¹ã®èšé²ãšããŠãã®ãŸãŸæ®ãããçŸåšã®package.json/LICENSE/ README / THIRD-PARTY-NOTICES.md ã¯ãã¹ãŠ MIT ãæããŠããã
- ãã³ãã¬ãŒãã®
setupã¹ã¯ãªãããšscripts/setup.jsã¯æ¡çšããªããæ¢åãªããžããªã§ãããã¬ãŒã¹ãã«ãã®çœ®æãäžèŠãªããã check:buildã远å ïŒÂ§5.8ïŒã@types/nodeã¯enginesã«åãããŠ^22ïŒãã³ãã¬ãŒãæºæ ïŒãã©ã³ã¿ã€ã ã Node 22 ãªã®ã§ 25 ç³»ã®åãå ¥ãããšååšããªã API ãåäžéã£ãŠããŸãã
ãã³ãã¬ãŒãã®ãã®ããã®ãŸãŸäœ¿ããfiles.includes ã« examples/** ãè¶³ãã ãã
{
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"ignoreUnknown": true,
"includes": ["src/**", "scripts/**", "examples/**", "*.ts", "*.json"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"linter": {
"enabled": true,
"rules": { "preset": "recommended" }
},
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded",
"trailingCommas": "all"
}
}
}ã³ãŒãã¹ã¿ã€ã«ïŒãã³ãã¬ãŒãç±æ¥ãæ¬èšèšæžã®ã³ãŒãäŸãããã«åŸã£ãŠããïŒ
- ã»ãã³ãã³ãªãïŒ
semicolons: "asNeeded"ïŒ - ã·ã³ã°ã«ã¯ã©ãŒã
- æ«å°Ÿã«ã³ããã
- ã€ã³ãã³ã 2 ã¹ããŒã¹ãè¡å¹ 100
- æ¹è¡ LF
console.warn / console.info ããã©ã³ãé¢é£ïŒÂ§8.3ïŒã§äœ¿ãããBiome ã® recommended ããªã»ããã« noConsole ã¯å«ãŸããªããã远å èšå®ã¯äžèŠã
ãã³ãã¬ãŒãã®ãã®ããã®ãŸãŸäœ¿ãããã¹ã㯠src é
äžã«ãããã include ã®è¿œå ã¯äžèŠã
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2024"],
"strict": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"declaration": true,
"isolatedModules": true,
"resolveJsonModule": true
},
"include": ["src", "tsdown.config.ts", "vitest.config.ts"]
}
noUncheckedIndexedAccess/exactOptionalPropertyTypesã¯ãã³ãã¬ãŒãã«å«ãŸããŠããªããã远å ããªãããã³ãã¬ãŒããšã®å·®ç°ãå¢ãããªãããšãåªå ãããå¿ èŠã«ãªã£ãæç¹ã§å¥éè°è«ããã
ãã³ãã¬ãŒãã® .gitattributes 㯠1 è¡ã®ã¿ããã€ããªã¯ Git ã NUL ãã€ãã§èªåå€å®ãããããsrc/__fixtures__/*.png ã眮ããŠãåé¡ã¯èµ·ããªãããã³ãã¬ãŒãã®ãŸãŸæ¡çšããã
# Normalize all text files to LF, regardless of the platform or the
# contributor's core.autocrlf setting, so Biome's formatter (which writes LF)
# never disagrees with what's checked out on Windows.
* text=auto eol=lf.editorconfig ã¯ãã³ãã¬ãŒãã«ååšããªããããšãã£ã¿åŽã§ã LF ãæ
ä¿ãããã远å ããïŒBiomeã»Git ãšåãã㊠3 éïŒã
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = falseæ¬ãªããžããªã¯ Windows ã§éçºãããŠããããã
.gitattributeså°å ¥æã«äžåºŠã ãæ¢åãã¡ã€ã«ã®æ£èŠåãå¿ èŠãgit add --renormalize . && git commit -m "Normalize line endings to LF"
ãã³ãã¬ãŒãã®ãã®ããã®ãŸãŸæ¡çšããïŒ.private/ ãæ¢ã«å«ãŸããŠããïŒãçŸè¡ã® tests ãšã³ããªã¯äžèŠã«ãªãã
.private/
node_modules/
dist/
coverage/
*.log
.DS_Store
.env
.env.*
!.env.exampleãã³ãã¬ãŒãã®æ§æã«ããšã³ããªè¿œå ãšäŸåã®å€éšåèšå®ãå ããã
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts', 'src/api/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
// Node >= 22 supports ES2024
target: 'node22',
platform: 'node',
deps: {
// ky is ESM-only; inline it so the CJS build doesn't `require()` an ESM package.
alwaysBundle: ['ky'],
// Native bindings must stay as imports â a bundled .node file won't load.
neverBundle: ['@napi-rs/canvas', '@twemoji/parser', 'budoux'],
},
})å®è£ æã®èšæ£: tsdown ã®ç§»è¡ã¬ã€ãã«ã¯
deps: { external, noExternal }ãšãããã0.22.14 ã®å®éã®DepsConfigã¯neverBundle/alwaysBundleã«æ¹åãããŠãããæ§åãæžããšåãšã©ãŒã«ãªãã
src/api/index.ts ããšã³ããªã«è¿œå ãããšãdist/api/index.{mjs,cjs,d.mts,d.cts} ãåºåããã§5.1 ã® exports ãšå¯Ÿå¿ããã
tsup ããã®äž»ãªå·®åïŒç§»è¡æã®æ³šæïŒ
| tsup | tsdown |
|---|---|
external / noExternal ããããã¬ãã« |
deps: { neverBundle, alwaysBundle } ã«æ¹åïŒ0.22 ã§åç§°å€æŽïŒ |
| CJS ãæ¢å® | ESM ãæ¢å®ãformat ãæç€ºãã |
cjsInterop |
cjsDefault |
ESM / CJS 䞡察å¿ã®æ³šæç¹
| é ç® | æ±ã |
|---|---|
ky |
deps.alwaysBundle ã§ãã³ãã«ãCJS ããã® require('ky') ãåé¿ |
@napi-rs/canvas |
externalã.node ãã€ããªãå«ããããã³ãã«äžå¯ |
@twemoji/parser / budoux |
CJS/ESM äž¡å¯Ÿå¿æžã¿ãexternal ã®ãŸãŸã§è¯ã |
import.meta |
CJS åºåã§å£ãããã src/ å
ã§äœ¿çšçŠæ¢ããã¹è§£æ±ºã¯ node:os / node:path ã§çµãïŒscripts/ å
㯠ESM å°çšãªã®ã§äœ¿çšå¯ïŒ |
| Top-level await | åäžãsrc/ å
ã§äœ¿çšçŠæ¢ |
ãã³ãã¬ãŒãã« coverage èšå®ã远å ããã
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/__fixtures__/**'],
},
},
})ãã«ãææç©ã«å¯Ÿããæ€èšŒãvitest ã® include ã src/** ã«éå®ãããŠããããŸã dist ãå¿
èŠãªãããç¬ç«ããã¹ã¯ãªãããšã㊠CI ã® build ãžã§ãã§å®è¡ããã
æ€èšŒé ç®:
require('./dist/index.cjs')ãæåããMiQã export ããŠããimport('./dist/index.mjs')ãæåããMiQã export ããŠããrequire('./dist/api/index.cjs')/import('./dist/api/index.mjs')ãæåããVoidsMiQã export ããŠããdist/api/index.{cjs,mjs}ã®ãœãŒã¹æååã«@napi-rs/canvasãšbudouxãçŸããªãïŒÂ§4.2 ã®äŸååé¢ã®æ ä¿ïŒdist/**ã«require("ky")ãçŸããªãïŒdeps.alwaysBundleãå¹ããŠããããšã®ç¢ºèªïŒdist/**ã« CRLF ãæ··å ¥ããŠããªãdist/index.d.mts/dist/api/index.d.ctsãªã©ã®åå®çŸ©ãçæãããŠãã
äŸåãå¢ãããªããã node:assert ãš node:fs ã®ã¿ã§æžããBiome ã® files.includes ã« scripts/** ãå«ãŸããŠããã®ã§ lint/format 察象ã«ãªãã
| é ç® | ãã³ãã¬ãŒã | æ¬ããã±ãŒãž | çç± |
|---|---|---|---|
exports |
. ã®ã¿ |
. + ./api + ./package.json |
ãµããã¹åå²ïŒÂ§4.1ïŒ |
tsdown.entry |
1 ãšã³ã㪠| 2 ãšã³ã㪠| åäž |
tsdown.deps |
ãªã | alwaysBundle: ['ky'], neverBundle: ['@napi-rs/canvas', âŠ] |
ESM-only äŸåãšãã€ãã£ããã€ããªïŒÂ§5.6ïŒ |
scripts/setup.js |
ãã | åé€ | æ¢åãªããžããªã§ãã¬ãŒã¹ãã«ã眮æãäžèŠ |
scripts/check-build.js |
ãªã | 远å | äŸååé¢ãšãã¥ã¢ã«ãã©ãŒãããã®æ€èšŒïŒÂ§5.8ïŒ |
biome.files.includes |
src, scripts, *.ts, *.json |
+ examples/** |
ãµã³ãã«ã³ãŒãã lint 察象ã«ãã |
vitest.coverage |
ãªã | 远å | test:coverage ã¹ã¯ãªãããååšããããèšå®ãæç€º |
.editorconfig |
ãªã | 远å | ãšãã£ã¿åŽã§ã LF ãæ ä¿ |
| CI ãããªã¯ã¹ | ubuntu à Node 22/24 |
+ windows / macos / alpine |
ãã€ãã£ããã€ããªãå«ãããïŒÂ§5.10ïŒ |
.github/dependabot.yml |
ãªã | åé€ïŒçŸè¡ããïŒ | æç€ºã©ãã廿¢ïŒÂ§2.2ïŒ |
.github/FUNDING.yml |
ãªã | ç¶æïŒçŸè¡ã®ãŸãŸïŒ | æ¢åã®èšå®ãå£ããªã |
ãã³ãã¬ãŒãã® <setup-steps> / <pm> ã npm åãã«å±éããããã§ããã©ãããã©ãŒã ãããªã¯ã¹ãæ¡åŒµããã
@napi-rs/canvas ã¯ãã©ãããã©ãŒã ããšã«å¥ãã€ããªãèªã¿èŸŒããããubuntu ã ãã§ã¯ Windowsã»macOSã»musl ã®ç Žæãæ€åºã§ããªããããã¯ãã³ãã¬ãŒãããæå³çã«éžè±ããã
name: ci
on:
push:
branches: [main, release/*]
pull_request:
branches: [main, release/*]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm install
- run: npm run ci
- run: npm run typecheck
- run: npm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm install
- run: npm run build
- run: npm run check:build
alpine:
runs-on: ubuntu-latest
container: node:22-alpine
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run testfail-fast: false ã«ããã®ã¯ãç¹å®ãã©ãããã©ãŒã ã ãã§èœã¡ããšãã«ä»ã®çµæãèŠããããã
npm run ciïŒ= biome ciïŒã¯å
š OS ã§èµ°ããã.gitattributes ã«ããäœæ¥ããªãŒã LF ã«æ£èŠåãããã®ã§ãWindows ã§ããã©ãŒãããå·®åã¯åºãªãã
ãã³ãã¬ãŒãã®ãã®ã npm åãã«å±éããŠãã®ãŸãŸäœ¿ãã**npm Trusted PublishingïŒOIDCïŒ**ã«ãã NPM_TOKEN ãæããªãã
name: release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release: a semver bump (patch/minor/major/prerelease) or an explicit version (e.g. 0.1.0)'
required: true
default: patch
permissions:
contents: write # push the version commit/tag and create the GitHub Release
id-token: write # npm trusted publishing (OIDC)
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm install
- run: npm run ci
- run: npm run typecheck
- run: npm run test
- run: npm run build
- run: npm run check:build # â å·®å: å
¬éåã«ææç©ãæ€èšŒ
- name: Bump version
id: bump
run: |
OLD="$(node -p "require('./package.json').version")"
npm version "${{ inputs.version }}" --no-git-tag-version --allow-same-version >/dev/null
NEW="$(node -p "require('./package.json').version")"
echo "version=v${NEW}" >> "$GITHUB_OUTPUT"
if [ "$OLD" = "$NEW" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Version is already ${NEW}; skipping the bump commit but still releasing."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- run: npm install -g npm@11
- run: npm publish --access public --provenance
- name: Commit, tag and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [ "${{ steps.bump.outputs.changed }}" = "true" ]; then
git commit -am "release: ${{ steps.bump.outputs.version }}"
fi
if ! git rev-parse -q --verify "refs/tags/${{ steps.bump.outputs.version }}" >/dev/null; then
git tag "${{ steps.bump.outputs.version }}"
fi
git push origin HEAD "${{ steps.bump.outputs.version }}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${{ steps.bump.outputs.version }}" --title "${{ steps.bump.outputs.version }}" --generate-noteså
¬éåã®æºåïŒ1 åã ãïŒ: npmjs.com ã® makeitaquote â Settings â Publishing access â Trusted publishers ã§ GitHub ã® otnc/makeitaquote / release.yml ãç»é²ããããããããªããš npm publish ã倱æããã
ãã³ãã¬ãŒãã®ä»¥äžããã®ãŸãŸæã¡èŸŒã¿ããã¬ãŒã¹ãã«ããæ¬ãªããžããªã®å€ã«çœ®æããã
.github/ISSUE_TEMPLATE/1-bug-report.yml.github/ISSUE_TEMPLATE/2-feature-request.yml.github/ISSUE_TEMPLATE/config.yml.github/PULL_REQUEST_TEMPLATE.mdCONTRIBUTING.md
ãã°å ±åãã³ãã¬ãŒãã«ã¯ãæ¬ããã±ãŒãžåºæã®é ç®ãšããŠä»¥äžã远å ããã
- OS / ã¢ãŒããã¯ãã£ïŒ
@napi-rs/canvasã®ãã€ããªéžæã«çŽçµïŒ - Node.js ã®ããŒãžã§ã³
- å®è¡ç°å¢ïŒããŒã«ã« / Docker / Alpine / ãµãŒããŒã¬ã¹ïŒ
- 䜿çšããŠãããšã³ããªïŒ
makeitaquoteãmakeitaquote/apiãïŒ
export { MiQ } from './core/MiQ'
export { defineTheme, themes } from './theme/presets'
export { fonts } from './font/registry'
export {
AssetFetchError,
FontNotAvailableError,
MiQError,
RenderError,
ValidationError,
} from './core/errors'
export type {
AutoFontOptions,
EncodeOptions,
FontSource,
MessageLike,
MiQOptions,
OutputFormat,
QuoteData,
QuoteInput,
Segment,
Theme,
ThemeInput,
ThemeName,
} from './core/types'class MiQ {
constructor(options?: MiQOptions)
// ---- å
¥å ----
setText(text: string): this
setAvatar(avatar: string | URL | Buffer | Uint8Array | null): this
setUsername(username: string): this
setDisplayName(displayName: string): this
setWatermark(watermark: string): this
setFromMessage(message: MessageLike): this
setFromObject(input: QuoteInput): this
// ---- èŠãç® ----
setTheme(theme: ThemeName | ThemeInput): this
setSize(width: number, height: number): this
// ---- ååŸ ----
getData(): Readonly<QuoteData>
getTheme(): Readonly<Theme>
clone(): MiQ
// ---- åºå ----
render(): Promise<Canvas>
toBuffer(format?: OutputFormat, options?: EncodeOptions): Promise<Buffer>
toStream(format?: OutputFormat, options?: EncodeOptions): Promise<Readable>
toDataURL(format?: OutputFormat, options?: EncodeOptions): Promise<string>
}
interface MiQOptions {
theme?: ThemeName | ThemeInput
/** æ¢å® trueãæªç»é²ãã©ã³ãã URL ããèªåååŸããïŒÂ§8ïŒ */
autoFont?: boolean | AutoFontOptions
/** true ã«ãããšæªç»é²ãã©ã³ãã§ FontNotAvailableError ãæãã */
strictFonts?: boolean
/** ã¢ã»ããååŸå€±ææã®æåãæ¢å® 'text' */
onAssetError?: 'ignore' | 'text' | 'throw'
signal?: AbortSignal
}import { MiQ } from 'makeitaquote'
import { writeFile } from 'node:fs/promises'
// ãã©ã³ãèšå®ã¯äžèŠãååã®ã¿ Noto Sans JP ãèªåååŸãããïŒÂ§8ïŒ
const png = await new MiQ()
.setText('åŸèŒ©ã¯ç«ã§ãããååã¯ãŸã ç¡ãã')
.setAvatar('https://cdn.discordapp.com/avatars/.../avatar.webp?size=512')
.setUsername('otoneko.')
.setDisplayName('é³ç«ïœ¡')
.setWatermark('Make it a Quote')
.setTheme('dark')
.toBuffer('png')
await writeFile('quote.png', png)// discord.js ãã
import { AttachmentBuilder } from 'discord.js'
import { MiQ } from 'makeitaquote'
const buffer = await new MiQ().setFromMessage(message).toBuffer('png')
await message.reply({
files: [new AttachmentBuilder(buffer, { name: 'quote.png' })],
})// ããŒãã®ã«ã¹ã¿ãã€ãº
const buffer = await new MiQ()
.setFromMessage(message)
.setTheme({
extends: 'light',
background: '#FFF8E7',
text: { color: '#2B2B2B', font: 'Noto Serif JP' },
avatar: { grayscale: false, position: 'right' },
})
.setSize(1600, 900)
.toBuffer('webp', { quality: 90 })v12 ã§å»æ¢: ãã®ç¯ã®å 容ã¯ãã®ãŸãŸ
@makeitaquote/voidsãžç§»èšããããååã»æåãšã倿Žãªãã§19 ãåç §ã
§2.1 ã§ç¢ºèªãããšã³ããã€ã³ãã®æ©èœå·®ãããã®ãŸãŸ API 圢ç¶ã«åæ ããã
export class VoidsMiQ {
constructor(options?: VoidsOptions)
setText(text: string): this
setAvatar(avatar: string | null): this
setUsername(username: string): this
setDisplayName(displayName: string): this
setColor(color: boolean): this
setWatermark(watermark: string): this
setFromMessage(message: MessageLike): this
setFromObject(input: QuoteInput): this
getData(): Readonly<QuoteData>
/**
* ãã¹ããããç»å URL ãååŸããã
* â åžžã« /fakequote ã䜿ãïŒURL ãè¿ããã®ã¯ãã¡ãã ãïŒ
* çæç»å㯠Voids åŽã«ã¢ããããŒããããç¹ã«æ³šæã
*/
toURL(): Promise<string>
/**
* ç»åãã€ããªãååŸããã
* â æ¢å®ã§ /fakequotebeta ã䜿ãïŒ1 åŸåŸ©ã§æžã¿ãç»åãå€éšã«ä¿åãããªãïŒ
* â { hosted: true } ã§ /fakequote çµç±ïŒPOST â URL â GET ã® 2 åŸåŸ©ïŒã«åãæ¿ãã
*/
toBuffer(options?: { hosted?: boolean }): Promise<Buffer>
}
export interface VoidsOptions {
baseUrl?: string // æ¢å® 'https://api.voids.top'
timeout?: number // æ¢å® 15000
retry?: number // æ¢å® 2
headers?: Record<string, string>
signal?: AbortSignal
}
export class VoidsApiError extends MiQError {
readonly status?: number
readonly body?: unknown
readonly endpoint: '/fakequote' | '/fakequotebeta'
}
/** å®è£
ã®ãã©ãŒã«ããã¯çšã«ããšã³ããã€ã³ãã®èœåãå
¬éãã */
export const endpoints: {
readonly hosted: { path: '/fakequote'; returns: 'url'; roundTripsForBuffer: 2 }
readonly direct: { path: '/fakequotebeta'; returns: 'binary'; roundTripsForBuffer: 1 }
}v8 ããã®æ¹åç¹
- ãã¡ãœããåã§ãšã³ããã€ã³ããéžã¶ãïŒ
generate/generateBetaïŒã®ããããæ¬²ããææç©ãèšãã°é©åãªãšã³ããã€ã³ããéžã°ããããã«ããã toBuffer()ãæ¢å®ã§/fakequotebetaã䜿ãããšã§ãv8 ã®generate(true)ã«ãã£ãç¡é§ãª 2 åŸåŸ©ãæ¶ããã- ç»åãå€éšã«ã¢ããããŒããããã®ã¯
toURL()ïŒããã³toBuffer({ hosted: true })ïŒãåŒãã ãšãã ãã§ããããšããã³ãŒãäžæç€ºãããã - ã¯ã©ã¹åã¯
VoidsMiQãMiQãšããå¥åãåæã« export ããimport { MiQ } from 'makeitaquote/api'ã§ãæžããããã«ããïŒããŒã«ã«çãšäœµçšããå Žåã¯VoidsMiQã䜿ãïŒã
QuoteData / MessageLike / ãšã©ãŒåºåº MiQError ã¯ã«ãŒããš api ã§å
±æããã
import { MiQ } from 'makeitaquote'
import { VoidsMiQ } from 'makeitaquote/api'
const data = new MiQ().setFromMessage(message).getData()
// API ãåªå
ããèœã¡ãŠãããããŒã«ã«ã«ãã©ãŒã«ããã¯ãã
let png: Buffer
try {
png = await new VoidsMiQ().setFromObject(data).toBuffer()
} catch {
png = await new MiQ().setFromObject(data).toBuffer('png')
}æ¢å®ãã£ã³ãã¹: 1280 à 720ïŒ16:9ïŒã座æšã¯å¹
W / é«ã H ã«å¯Ÿããæ¯çã§å®çŸ©ããsetSize() ã«è¿œéãããã
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ââââââ â
â âââââââââââââââââââ ââ â
â â â ââ â
â â avatar (cover) â ââ â åŒçšæãããã« â
â â grayscale â ââ å
¥ããé·ããã° â
â â â ââ æãè¿ããã â â
â â â ââ â
â â â ââ - é³ç«ïœ¡ â
â â â ââ @otoneko. â
â âââââââââââââââââââ ââ â
â Make it a Quote â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
0 0.5W 0.62W W
ââ ã°ã©ããŒã·ã§ã³ ââ
| èŠçŽ | æ¢å®å€ |
|---|---|
| ãã£ã³ãã¹ | 1280 à 720ãèæ¯ #000000 |
| ã¢ãã¿ãŒé å | x: 0 â 0.5Wãy: 0 â Hãobject-fit: cover çžåœã§ã¯ããã |
| ã°ã¬ãŒã¹ã±ãŒã« | dark / light ã¯æå¹ãcolor ã¯ç¡å¹ |
| ãã§ãŒã | createLinearGradient(0.30W, 0, 0.62W, 0)ïŒrgba(bg,0) â rgba(bg,1)ãã¹ããã㯠0 / 0.55 / 1 ã® 3 ç¹ |
| ããã¹ãé å | x: 0.54W â 0.96WïŒå¹
0.42WïŒãy: 0.10H â 0.78H |
| æ¬æãã©ã³ã | åæ 0.062HïŒ=45px @720ïŒãèªåçž®å°ïŒÂ§7.4ïŒãtextAlign: centerãè¡éã 1.35em |
| åŒçšç¬Š | æ¬æã â â ã§å²ãïŒããŒãã§å€æŽã»ç¡å¹åå¯èœïŒ |
| display name | æ¬æãããã¯äž 0.055Hããµã€ãº 0.040Hã- {displayName} |
| username | display name äž 0.030Hããµã€ãº 0.028Hãè² #9A9A9Aã@{username} |
| watermark | å³äž x: 0.96W, y: 0.94HãtextAlign: rightããµã€ãº 0.024Hãè² #6E6E6E |
light ããŒãã¯èæ¯ #FFFFFFãæ¬æ #111111ãusername #666666ãwatermark #999999ãã°ã©ããŒã·ã§ã³çµç«¯è²ãçœã«å€æŽã
1. validate(data) å¿
é é
ç®ã»åã®æ€èšŒ â ValidationError
2. resolveTheme(themeInput) éšåæå® â å®å
šãª Theme
3. ensureFonts(theme) å¿
èŠãã©ã³ãã®ç¢ºèªã»èªåååŸ â §8
4. prefetchAssets(data) ã¢ãã¿ãŒ + çµµæåã䞊åååŸ â AssetFetchError
ââ loadAvatar()
ââ prefetchEmojis() ã»ã°ã¡ã³ãèµ°æ» â URL éå â 䞊å fetch
5. createCanvas(W, H)
6. drawBackground()
7. drawAvatar() cover ã¯ããã â ã°ã¬ãŒã¹ã±ãŒã« â drawImage
8. drawGradient()
9. layoutText() ã»ã°ã¡ã³ãå â ãã£ããã£ã³ã° â æãè¿ã
10. drawQuote()
11. drawAttribution()
12. drawWatermark()
13. return canvas
4 ã§å
šã¢ã»ãããå
èªã¿ããã®ãèŠç¹ãåèã©ã€ãã©ãªã¯æç»ã«ãŒãå
ã§ await loadImage(url) ããŠããããçŽå HTTP ã«ãªã£ãŠãããv9 ã§ã¯ 9 ã§ç¢ºå®ããã»ã°ã¡ã³ãéåãã URL ãéè€æé€ããPromise.allSettled ã§äžŠåååŸããŠãã 10 ã«å
¥ãã
ã»ã°ã¡ã³ãå
type Segment =
| { kind: 'text'; value: string }
| { kind: 'emoji'; source: 'twemoji'; url: string; raw: string }
| {
kind: 'emoji'
source: 'discord'
url: string
raw: string
id: string
name: string
animated: boolean
}ã»ã°ã¡ã³ãåïŒtext/segment.tsïŒ
@twemoji/parserã®parse(text, { assetType: 'png' })ã§æšæºçµµæåã®indicesãšurlãåŸãã- çµµæååºéã§åå²ããéã®ããã¹ãã
{ kind: 'text' }ãšããŠç©ãã - åããã¹ãçã«
/<(a)?:(\w{2,32}):(\d{17,20})>/gãé©çšããŠããã«åå²ãããURL ã¯https://cdn.discordapp.com/emojis/{id}.{ext}?size=64ïŒanimatedãªã.gifïŒãGIF 㯠1 ãã¬ãŒã ç®ã®ã¿æç»ããããããã¯ä»æ§ãšã㊠README ã«æèšããã - 空æåã»ã°ã¡ã³ããé€å»ããã
å¹
èšæž¬ïŒtext/measure.tsïŒ
width(segment) =
kind === 'text' â ctx.measureText(value).width
kind === 'emoji' â fontSize + emojiSideMargin * 2 (æ¢å® margin = fontSize * 0.08)
çµµæåã¯ããŒã¹ã©ã€ã³åºæºã§ drawImage(img, x + sideMargin, y - fontSize + topMargin - baseline, fontSize, fontSize) ã«é
眮ããïŒtopMargin = fontSize * 0.10ïŒã
æ¹è¡åè£äœçœ®ã®æ±ºå®ïŒtext/breakpoint.tsïŒ
3 å±€ã§åè£ãäœããåªå 床ã®é«ãé ã«æ¡çšããã
| åªå 床 | çš®å¥ | æ±ºå®æ¹æ³ |
|---|---|---|
| 1 | ããŒãæ¹è¡ | å
¥åäžã® \nãå¿
ãããã§æã |
| 2 | åèªå¢çïŒã©ãã³ç³»ïŒ | åè§ã¹ããŒã¹ã»ã¿ãã»ãã€ãã³ã®çŽåŸ |
| 2 | æç¯å¢çïŒæ¥æ¬èªïŒ | BudouX ã® new Parser(jaModel).parse() ãè¿ããã£ã³ã¯å¢ç |
| 3 | çµµæåå¢ç | çµµæåã»ã°ã¡ã³ãã®ååŸ |
| 4 | ã°ã©ãã§ã å¢çïŒæçµææ®µïŒ | Intl.Segmenter(locale, { granularity: 'grapheme' }) |
BudouX ã®é©çšåäœã¯ãé£ç¶ããããã¹ãã»ã°ã¡ã³ãããçµµæåãæããšæèãåãããããçµµæåã§åºåãããåããã¹ãçã«å¯ŸããŠåå¥ã« parse() ãããBudouX ãåäžãã£ã³ã¯ããè¿ããªãïŒïŒåå²ã§ããªãïŒå Žåã¯åªå
床 4 ã®ã°ã©ãã§ã å¢çã«èœã¡ãã
æãè¿ãïŒtext/wrap.tsïŒ
- ããŒãæ¹è¡ã§åå²ã
- åè¡ã«ã€ããŠåè£äœçœ®ã€ãã®ããŒã¯ã³åãäœãã
- 貪欲æ³ã§è©°ãã
maxWidthè¶ éæã«çŽåã®æãåªå 床ã®é«ãåè£äœçœ®ã§æãã - åè£ããªãé·å€§ããŒã¯ã³ïŒé£ç¶ããè±æ°åã»URL ãªã©ïŒã¯ã°ã©ãã§ã åäœã§åŒ·å¶åå²ããã
- ç°¡æçŠååŠç:
- è¡é çŠåïŒåè¡ãžè¿œãåºãïŒ:
ããïŒïŒïŒïŒœïœããããããïŒïŒãããŒããã ããã£ãã ãããµã¶ãšå°æžãä»®å - è¡æ«çŠåïŒæ¬¡è¡ãžéãïŒ:
ïŒïŒ»ïœãããããã
- è¡é çŠåïŒåè¡ãžè¿œãåºãïŒ:
- ã°ã©ãã§ã ã¯ã©ã¹ã¿ãåæããªãããšã
Intl.Segmenterã§ä¿èšŒããïŒãµãã²ãŒããã¢ã»çµåæåã»ZWJ çµµæååïŒã
maxFontSize = theme.text.size (æ¢å® 0.062H)
minFontSize = theme.text.minSize (æ¢å® 0.030H)
for size = maxFontSize down to minFontSize, step -1:
lines = wrap(segments, size, areaWidth)
blockHeight = lines.length * size * lineHeight
if blockHeight <= areaHeight: break
if åãŸããªã:
theme.text.overflow === 'shrink' â minFontSize ã§æç»ïŒã¯ã¿åºãã¯ã¯ãªããïŒ
theme.text.overflow === 'ellipsis' â æ«å°Ÿã 'âŠ' ã«åãè©°ãïŒæ¢å®ïŒ
theme.text.overflow === 'error' â RenderError ãæãã
ç·åœ¢æ¢çŽ¢ã¯æå€§ 30 åçšåºŠã® wrap() åŒã³åºãã«ãªãããmeasureText çµæã Map<string, number> ã§ã¡ã¢åããBudouX ã® parse() çµæããã©ã³ããµã€ãºã«äŸåããªãããäžåºŠã ãèšç®ããŠãã£ãã·ã¥ããã°å®æž¬ 1ms æªæºã«åãŸããäºåæ¢çŽ¢ã¯ããµã€ãºãäžãããšå¿
ãåãŸãããçŠååŠçã®åœ±é¿ã§å調ã«ãªããªãçšãªã±ãŒã¹ãããããæ¡ããªãã
// 1. èªã¿èŸŒã¿: URL / Buffer / null
// null ãŸãã¯ååŸå€±æ â theme.avatar.fallbackïŒæ¢å®: åè² + ã€ãã·ã£ã«æåïŒ
// 2. cover ã¯ããã
const scale = Math.max(destW / img.width, destH / img.height)
const sw = destW / scale
const sh = destH / scale
const sx = (img.width - sw) / 2
const sy = (img.height - sh) / 2
// 3. ã°ã¬ãŒã¹ã±ãŒã«
// 第äžéžæ: ctx.filter = 'grayscale(100%)' â drawImage â ctx.filter = 'none'
// ãã©ãŒã«ããã¯: ãªãã¹ã¯ãªãŒã³ã§ getImageData â èŒåºŠå€æ â putImageData
// ïŒèµ·åæã« 1x1 canvas ã§ filter 察å¿ãæ€åºãçµæããã£ãã·ã¥ïŒ
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, destW, destH)èŒåºŠåŒã¯ Rec.709ïŒ0.2126R + 0.7152G + 0.0722BïŒã
theme.avatar.shape ã§ 'rect'ïŒæ¢å®ã»ãªãªãžãã«æºæ ïŒ/ 'circle' / 'rounded' ãéžã¹ããåŸè
2 ã€ã¯ ctx.clip() ã§åãæãã
Skia ã¯ã·ã¹ãã ãã©ã³ããæŸãããAlpine ã³ã³ãããæå°æ§æã® Linux ã«ã¯æ¥æ¬èªãã©ã³ããååšããªããäœãããªããã°æ¥æ¬èªãè±è ïŒâ¡ïŒã«ãªããäžæ¹ã§æ¥æ¬èªãã©ã³ãã npm ããã±ãŒãžã«å梱ãããš 10MB è¿ãå¢ããã
解決ç: ãã©ã³ãã npm ã«ã¯å梱ãããå¿ èŠã«ãªã£ãæç¹ã§ CDN ããååŸãããã£ã¹ã¯ã«ãã£ãã·ã¥ããã
@napi-rs/canvas ã® WOFF2 察å¿ãããŒãžã§ã³ã«ããäžå®å®ãªãããTTF ã«éå®ãããå®åšãšå°éæ§ãç¢ºèªæžã¿ã
| çšé | URL | ãµã€ãº |
|---|---|---|
| æ¥æ¬èªïŒæ¢å®ïŒ | https://cdn.jsdelivr.net/gh/google/fonts@main/ofl/notosansjp/NotoSansJP%5Bwght%5D.ttf |
9.6 MB |
| ã©ãã³ïŒæ¢å®ïŒ | https://cdn.jsdelivr.net/gh/google/fonts@main/ofl/notosans/NotoSans%5Bwdth,wght%5D.ttf |
2.0 MB |
ããããå¯å€ãã©ã³ãïŒVariable FontïŒã§å
šãŠã§ã€ããå«ããgoogle/fonts ã«éçã€ã³ã¹ã¿ã³ã¹ïŒstatic/NotoSansJP-Regular.ttfïŒã¯ååšããªãããšãç¢ºèªæžã¿ïŒ404ïŒã
@fontsource/* ã® WOFF2 ãµãã»ããïŒ1 ãã¡ã€ã«æ°çŸ KBïŒã®ã»ãã軜éã ããWOFF2 ã®èªã¿èŸŒã¿å¯åŠã確å®ã§ãªãããæ¢å®ã«ã¯ããªããfontSource ãªãã·ã§ã³ã§å©çšè
ãå·®ãæ¿ããããããã«ããã
ã«ã©ãŒçµµæåãã©ã³ãïŒNotoColorEmoji.ttf, 10.7 MBïŒã¯ååŸããªããçµµæå㯠Twemoji / Discord CDN ã®ç»åãšããŠæç»ããããäžèŠïŒÂ§3.3ïŒã
export const fonts = {
// ---- æç€ºçãªç»é² ----
registerFromPath(path: string, alias?: string): boolean
register(data: Buffer, alias: string): boolean
registerFromDir(dir: string): number
// ---- URL ããã®ååŸ ----
/** URL ããååŸããŠç»é²ããããã£ã¹ã¯ãã£ãã·ã¥ãçµç±ãã */
registerFromURL(url: string, alias: string): Promise<boolean>
/** æ¢å®ãã©ã³ãïŒNoto Sans JP / Noto SansïŒã確å®ã«çšæãã */
ensureDefaults(options?: AutoFontOptions): Promise<void>
// ---- åãåãã ----
families(): string[]
has(family: string): boolean
/** ãã£ã¹ã¯ãã£ãã·ã¥ã®å Žæãšäœ¿çšé */
cacheInfo(): { dir: string; files: string[]; bytes: number }
clearCache(): Promise<void>
}
export interface AutoFontOptions {
/** æ¢å® trueãfalse ã§èªåååŸãå®å
šã«ç¡å¹å */
enabled?: boolean
/** ããŠã³ããŒãå
ãæ¢å®ã¯äžèšã®è§£æ±ºé */
cacheDir?: string
/** ååŸãããã©ã³ãã®å®çŸ©ãæ¢å®ã¯ Noto Sans JP + Noto Sans */
sources?: FontSource[]
/** ããŠã³ããŒãã®ã¿ã€ã ã¢ãŠããæ¢å® 60000 */
timeout?: number
/** 鲿ã³ãŒã«ããã¯ãæªæå®ãªã console.info ã« 1 è¡åºã */
onProgress?: (info: { name: string; received: number; total: number }) => void
}
export interface FontSource {
family: string // 'Noto Sans JP'
url: string
/** ãã¡ã€ã«åãçç¥æã¯ URL ããå°åº */
fileName?: string
/** æŽåæ§æ€èšŒçšïŒä»»æïŒã'sha256-...' */
integrity?: string
}ãã£ãã·ã¥ãã£ã¬ã¯ããªã®è§£æ±ºé
AutoFontOptions.cacheDir- ç°å¢å€æ°
MIQ_FONT_CACHE_DIR $XDG_CACHE_HOME/makeitaquote/fonts~/.cache/makeitaquote/fontsïŒLinux/macOSïŒ/%LOCALAPPDATA%\makeitaquote\fontsïŒWindowsïŒos.tmpdir()/makeitaquote-fontsïŒäžèšãæžã蟌ã¿äžå¯ã®å ŽåïŒ
èªåååŸã®ããªã¬ãšæµã
render() ã® ensureFonts(theme):
1. theme.text.font ãªã©ã§èŠæ±ãããŠãããã¡ããªãåæ
2. GlobalFonts.families() ãšç»é²æžã¿ãšã€ãªã¢ã¹ã§è§£æ±ºã詊ã¿ã
3. ãã¹ãŠè§£æ±ºã§ãã â äœãããªãïŒãããã¯ãŒã¯ã¢ã¯ã»ã¹ãªãïŒ
4. 解決ã§ããªããã®ããã ã〠autoFont.enabled:
a. ãã£ã¹ã¯ãã£ãã·ã¥ã«ãã¡ã€ã«ãããã° registerFromPath ããŠçµäºïŒãããã¯ãŒã¯ãªãïŒ
b. ãªããã° CDN ããååŸ:
- ååã®ã¿ console.info ã§éç¥
ãmakeitaquote: downloading Noto Sans JP (9.6 MB) to ~/.cache/... (first run only)ã
- äžæãã¡ã€ã«ã«æžãåºã â å®äºåŸã« renameïŒéšåãã¡ã€ã«ãæ®ããªãïŒ
- registerFromPath ã§ç»é²
c. ååŸã«å€±æ â console.warn ãåºããã·ã¹ãã ãã©ã³ãã§æç»ãç¶è¡
5. autoFont.enabled === false ãã€æªè§£æ±º:
- strictFonts ãªã FontNotAvailableError
- ããã§ãªããã° console.warnïŒååã®ã¿ïŒãåºããŠç¶è¡
èšèšäžã®èŠç¹
- ãããã¯ãŒã¯ã¢ã¯ã»ã¹ã¯ãæ¬åœã«å¿ èŠãªãšãã®ååã ããã2 åç®ä»¥éã¯ãã£ã¹ã¯ãã£ãã·ã¥ããèªãããäžåçºçããªããã·ã¹ãã ã«æ¥æ¬èªãã©ã³ããããç°å¢ïŒå€ãã®éçºæ©ïŒã§ã¯æåããçºçããªãã
- ããŠã³ããŒãã¯äžæãã¡ã€ã« â rename ã® 2 段éãäžæãããéšåãã¡ã€ã«ã次åã«èªãŸããäºæ ãé²ãã
- åäžããã»ã¹å
ã®åæããŠã³ããŒã㯠in-flight promise ã§åæµããããè€æ°ã®
MiQãåæã«render()ããŠã 1 åããèœãšããªãã - è€æ°ããã»ã¹ã®åæããŠã³ããŒãã¯ãäžæãã¡ã€ã«åã« PID ãå«ããããšã§è¡çªãé¿ããïŒæåŸã« rename ãããã®ãåã€ãå 容ã¯åäžãªã®ã§åé¡ãªãïŒã
- é»ã£ãŠãããã¯ãŒã¯ã«åºãããšã¯ããªããååã¯å¿
ã 1 è¡ãã°ãåºãã
autoFont: falseã§å®å šã«ç¡å¹åã§ãããCI ãéåç°å¢åãã«ãMIQ_FONT_CACHE_DIRã«äºåé 眮ããŠããã°ãªãã©ã€ã³ã§åãã GlobalFonts.register()ã® Buffer åç §åé¡ïŒÂ§3.2ïŒãé¿ãããããèªåååŸãããã©ã³ãã¯å¿ ããã¡ã€ã«ã«æžããŠããregisterFromPath()ã§ç»é²ããã
ãã©ã³ã解決ã®åªå é
theme.*.fontã«æå®ããããã¡ããª- å©çšè ãæç€ºç»é²ãããã¡ããªïŒç»é²é ïŒ
- èªåååŸãããæ¢å®ãã©ã³ãïŒNoto Sans JP â Noto SansïŒ
- ã·ã¹ãã ãã©ã³ã
sans-serif
Docker ã§ã®æšå¥š
README ã«ããã«ãæã«ãã©ã³ããçŒã蟌ãã§ã©ã³ã¿ã€ã ã®ããŠã³ããŒãããªããäŸãèŒããã
ENV MIQ_FONT_CACHE_DIR=/app/.fonts
RUN node -e "import('makeitaquote').then(m => m.fonts.ensureDefaults())"export interface Theme {
width: number
height: number
background: string
avatar: {
grayscale: boolean
position: 'left' | 'right'
widthRatio: number // æ¢å® 0.5
shape: 'rect' | 'circle' | 'rounded'
fallback: { background: string; color: string } | null
}
gradient: {
enabled: boolean
startRatio: number // æ¢å® 0.30
endRatio: number // æ¢å® 0.62
stops: Array<[offset: number, alpha: number]> // æ¢å® [[0,0],[0.55,0.85],[1,1]]
}
text: {
color: string
font: string
size: number // 0 < v <= 1 ãªã H ã«å¯Ÿããæ¯çã> 1 ãªã px
minSize: number
lineHeight: number // æ¢å® 1.35
align: 'left' | 'center' | 'right'
quotes: [open: string, close: string] | null // æ¢å® ['â', 'â']
overflow: 'ellipsis' | 'shrink' | 'error'
area: { x: number; y: number; width: number; height: number }
/** æ¹è¡ã« BudouX ã䜿ãããæ¢å® true */
phraseBreak: boolean
/** BudouX ã®ã¢ãã«ãæ¢å® 'ja' */
locale: 'ja' | 'zh-hans' | 'zh-hant' | 'none'
}
displayName: { color: string; font: string; size: number; prefix: string }
username: { color: string; font: string; size: number; prefix: string }
watermark: {
color: string
font: string
size: number
position: 'bottom-right' | 'bottom-left' | 'bottom-center'
}
emoji: { sideMarginRatio: number; topMarginRatio: number; size: 64 | 72 | 128 }
}
export type ThemeInput = DeepPartial<Theme> & { extends?: ThemeName }
export type ThemeName = 'dark' | 'light' | 'color'| ããªã»ãã | èæ¯ | æ¬æ | ã¢ãã¿ãŒ | çšé |
|---|---|---|---|---|
dark |
#000000 |
#FFFFFF |
ã°ã¬ãŒã¹ã±ãŒã« | æ¢å®ããªãªãžãã« MiQ æºæ |
light |
#FFFFFF |
#111111 |
ã°ã¬ãŒã¹ã±ãŒã« | çœèæ¯ã»é»æå |
color |
#000000 |
#FFFFFF |
ã«ã©ãŒã®ãŸãŸ | v8 ã® setColor(true) çžåœ |
defineTheme(input: ThemeInput): Theme ã§ extends ã®è§£æ±ºãšæ¯çïŒçµ¶å¯Ÿå€ã®æ£èŠåãè¡ãã
util/lru.tsã«äŸåãªãã® LRUïŒMapã®æ¿å ¥é ãå©çšã~50 è¡ïŒãå®è£ ãlru-cacheãžã®äŸåã¯å¢ãããªãã- æ¢å®
maxEntries: 256/ttlMs: 3_600_000ã - ã¢ãžã¥ãŒã«ã¹ã³ãŒãã®ã°ããŒãã«ãã£ãã·ã¥ã
MiQã€ã³ã¹ã¿ã³ã¹åäœã§ã¯ãªã Bot å šäœã§å ±æãããã¹ãæ§è³ªã®ãã®ã - åäž URL ãžã®åæãªã¯ãšã¹ããåæµãããïŒin-flight promise ã®å ±æïŒã
- ååŸå€±æãã URL ã¯è² ã®ãã£ãã·ã¥ïŒæ¢å® 60 ç§ïŒã«å ¥ãããªãã©ã€åµãé²ãã
type OutputFormat = 'png' | 'jpeg' | 'webp' | 'avif'
interface EncodeOptions {
quality?: number // 1â100ãæ¢å® 92ãpng ã§ã¯ç¡èŠ
}| ã¡ãœãã | æ»ãå€ | åè |
|---|---|---|
toBuffer(format?, options?) |
Promise<Buffer> |
æ¢å® 'png' |
toStream(format?, options?) |
Promise<Readable> |
Readable.from(buffer) |
toDataURL(format?, options?) |
Promise<string> |
data:image/png;base64,... |
render() |
Promise<Canvas> |
å©çšè ãç¬èªã«åŸåŠçããéãé |
// src/http/client.ts
import ky from 'ky'
export const createClient = (options: HttpOptions = {}) =>
ky.create({
timeout: options.timeout ?? 10_000,
retry: {
limit: options.retry ?? 2,
methods: ['get', 'post'],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
backoffLimit: 3_000,
},
headers: options.headers,
})ã¢ã»ããååŸçšã»ãã©ã³ãååŸçšã»Voids API çšã§ã€ã³ã¹ã¿ã³ã¹ãåããïŒã¿ã€ã ã¢ãŠãå€ãç°ãªãããïŒãMiQOptions.signal ãå
šãªã¯ãšã¹ããžäŒæãããã
Error
ââ MiQError ïŒå
šãšã©ãŒã instanceof MiQError ãæºããïŒ
ââ ValidationError å
¥åã®åã»å¿
é ã»ç¯å²éåïŒfield ãæã€ïŒ
ââ FontNotAvailableError strictFonts æã®ãã©ã³ãæªè§£æ±º
ââ AssetFetchError ã¢ãã¿ãŒ / çµµæå / ãã©ã³ãã®ååŸå€±æïŒurl, causeïŒ
ââ RenderError æç»äžã®å€±æïŒoverflow: 'error' ãå«ãïŒ
ââ VoidsApiError Voids API ã®ãšã©ãŒïŒstatus, body, endpointïŒ
ãã¹ãŠ cause ãåãåãå
äŸå€ã倱ããªãã
ç»ååºåã®ãã¯ã»ã«æ¯èŒã¯ãã©ã³ãã®ããŒãžã§ã³å·®ã»OS å·®ã»Skia ã®æŽæ°ã§å®¹æã«å£ãããçŽé¢æ°ã®ããžãã¯ãåããã¹ãããæç»çµæã¯ smoke test ã«çããã
ãã¹ãã¯ãœãŒã¹ã«äœµçœ®ïŒsrc/**/*.test.tsïŒãããŒã¹ãã³ãã¬ãŒãã® vitest.config.ts ã® include ã«åŸãã
| 察象ãã¡ã€ã« | å 容 |
|---|---|
text/segment.test.ts |
å
¥åæåå â Segment[]ãTwemoji / Discord çµµæå / ZWJ çµµæåïŒðšâð©âð§âðŠïŒ/ èè²ä¿®é£Ÿ / æ··åš |
text/breakpoint.test.ts |
BudouX ã®æç¯å¢çãæåŸ
ã©ããåè£ã«ãªãããæ¥æ¬èªã»è±èªã»æ··åšãphraseBreak: false ã§ã°ã©ãã§ã å¢çã«èœã¡ãããš |
text/wrap.test.ts |
å¹
åºå®ã® fake measureText ãæ³šå
¥ããŠè¡åå²ãæ€èšŒãCJKã»é·å€§ URLã»çŠåã»æ¹è¡æ··åš |
text/fit.test.ts |
åãŸãïŒåãŸããªãïŒoverflow 3 çš® |
theme/resolve.test.ts |
extends ã®åæãæ¯çâpx æ£èŠåãäžæ£å€ã®æåŠ |
util/lru.test.ts |
远ãåºãé åºãTTLãin-flight åæµãè² ã®ãã£ãã·ã¥ |
emoji/resolve.test.ts |
Discord çµµæå URL ã®çæïŒanimated ã§æ¡åŒµåãå€ããããšãsize ãã©ã¡ãŒã¿ïŒ |
font/autoload.test.ts |
fetch ãã¹ã¿ãåãã(a) ãã£ãã·ã¥ãããæã«ãããã¯ãŒã¯ãåŒã°ããªãã(b) äžæãã¡ã€ã«ârenameã(c) åæåŒã³åºãã®åæµã(d) 倱ææã«äŸå€ãæãã warn ã§ç¶è¡ |
core/source.test.ts |
discord.js v13 / v14 / selfbot çžåœã®ã¢ãã¯ãã QuoteData ãå°åº |
core/MiQ.test.ts |
ããªããŒã·ã§ã³ãclone() ã®ç¬ç«æ§ããã§ãŒã³ã®æ»ãå€ |
render/pipeline.test.ts |
å®éã« PNG ãçæãã(a) PNG ã·ã°ããã£ã(b) å¹ ã»é«ãã(c) ç¹å®åº§æšã®è²ãèæ¯è²ãšäžèŽã(d) äŸå€ãåºãªã |
api/client.test.ts |
ky ãã¢ãã¯ããtoURL() ã /fakequote ããtoBuffer() ã /fakequotebeta ãå©ãããšãtoBuffer({ hosted: true }) ã 2 åŸåŸ©ããããšãsnake_case 倿ããšã©ãŒãããã³ã°ãæ€èšŒãå® API ã¯å©ããªã |
çµµæå CDNã»ãã©ã³ã CDN ãžã®ãããã¯ãŒã¯ã¢ã¯ã»ã¹ã¯ãã¹ãã§è¡ããªããemoji/loader.ts ãš font/autoload.ts ã® fetch ãæ³šå
¥å¯èœã«ããsrc/__fixtures__/ ãè¿ãã¹ã¿ãã䜿ãã
ãã«ãææç©ã®æ€èšŒã¯ scripts/check-build.js ãæ
åœããïŒÂ§5.8ïŒãvitest ã®å¯Ÿè±¡å€ã
| # | ãã§ãŒãº | å 容 | å®äºæ¡ä»¶ |
|---|---|---|---|
| 0 | æ¢åè³ç£ã®æŽç | index.js / index.d.ts / lib/ / examples/*.js,md / .github/dependabot.yml ãåé€ã.gitattributes å°å
¥ + git add --renormalize . |
äœæ¥ããªãŒã LF ã«çµ±äžããã |
| 1 | è¶³å Ž | oto-lab/npm-biome-ts ãã package.json / biome.json / tsconfig.json / tsdown.config.ts / vitest.config.ts / .gitignore / .editorconfig / .github/** ãç§»æ€ãã§5.9 ã®å·®åãé©çšãscripts/check-build.js äœæ |
npm run ci && npm run typecheck && npm run build && npm run check:build ãéã |
| 2 | åãšå ¥å | core/types.ts, errors.ts, quote.ts, source.ts |
core/source.test.ts ãç· |
| 3 | api | api/* ãå®è£
ããšã³ããã€ã³ãã®æ©èœå·®ïŒÂ§2.1ïŒãå®åºåã§æ€èšŒããREADME ã®å¯Ÿå¿è¡šãç¢ºå® |
v8 ãšåçã®çæãã§ããcheck:build ã®äŸååé¢ãã§ãã¯ãç· |
| 4 | ããã¹ãå±€ | text/segment.ts, measure.ts, breakpoint.ts(BudouX), wrap.ts, fit.ts |
æãè¿ãã»çŠåã»æç¯å¢çã®ãã¹ããç·ïŒæç»ãªãïŒ |
| 5 | çµµæåå±€ | emoji/*, util/lru.ts |
ã¹ã¿ã fetch ã§ã®ãã¹ããç· |
| 6 | ãã©ã³ãå±€ | font/registry.ts, autoload.ts, diskCache.ts, sources.ts |
ãã©ã³ãæªèšå®ã® Alpine ã³ã³ããã§æ¥æ¬èªãæç»ã§ãã |
| 7 | æç»å±€ | theme/*, render/*, output/encode.ts |
toBuffer() ãæå¹ãª PNG ãè¿ã |
| 8 | ä»äžã | README å
šé¢æ¹èšãexamples/* ã TS ã§æžãçŽããIssue/PR ãã³ãã¬ãŒããCONTRIBUTING |
CI å šãããªã¯ã¹ç· |
| 9 | å ¬é | npm ã® Trusted Publisher èšå®ïŒÂ§5.11ïŒâ release ã¯ãŒã¯ãããŒãæåå®è¡ |
9.0.0 ã publish ããã |
ãã§ãŒãº 3 ãå ã«çœ®ãçç±: api ã¯æ¢åã³ãŒãã®ç§»æ€ã§å®è£ éãå°ããããããŸã§ã§ãv8 çžåœã®æ©èœãæã€ v9ããåãç¶æ ãäœããã®ã§ã以éã®ããŒã«ã«æç»å®è£ ãèœã¡çããŠé²ããããã
ãã§ãŒãº 6 ããã§ãŒãº 7 ããåã«çœ®ãçç±: ãã©ã³ãããªããšæç»çµæãç®èŠç¢ºèªã§ããªãããã
- const { MiQ } = require('makeitaquote');
+ const { MiQ } = require('makeitaquote') // ããŒã«ã«çæã«ç§»è¡ããå Žå
+ const { VoidsMiQ } = require('makeitaquote/api') // API ã䜿ãç¶ããå Žå
const buffer = await new MiQ()
.setText(message.content)
.setAvatar(url)
.setUsername('otoneko.')
- .setDisplayname('é³ç«ïœ¡')
+ .setDisplayName('é³ç«ïœ¡')
- .setColor(true)
+ .setTheme('color')
.setWatermark('Make it a Quote')
- .generate(true);
+ .toBuffer('png')| v8 | v9ïŒããŒã«ã«ïŒ | v9ïŒAPIïŒ |
|---|---|---|
new MiQ() |
new MiQ() |
new VoidsMiQ() |
.setDisplayname(v) |
.setDisplayName(v) |
.setDisplayName(v) |
.setColor(true) |
.setTheme('color') |
.setColor(true) |
.setText(v, true) |
.setText(stripDiscordMarkdown(v)) |
åå·Š |
.generate() |
ããŒã«ã«çæã« URL ã¯ååšããªã | .toURL()ïŒ/fakequoteïŒ |
.generate(true) |
.toBuffer('png') |
.toBuffer({ hosted: true })ïŒ2 åŸåŸ©ïŒ |
.generateBeta() |
.toBuffer('png') |
.toBuffer()ïŒ/fakequotebetaã1 åŸåŸ©ïŒ |
.getFormat() |
.getData() |
.getData() |
ãã®ä»ã®ç Žå£ç倿Ž
setText()ã®ç¬¬ 2 åŒæ°ïŒMarkdown é€å»ïŒã¯å»æ¢ãdisplusäŸåãããããããå¿ èŠãªãå©çšè åŽã§ååŠçããã- Node.js 22 以äžãå¿ é ã
makeitaquote/voidsã§ã¯ãªãmakeitaquote/apiã- ååå®è¡æã«æ¥æ¬èªãã©ã³ãïŒ9.6 MBïŒãèªåããŠã³ããŒããããå Žåãããã
autoFont: falseã§ç¡å¹åã§ããïŒÂ§8.3ïŒã
README ã§èª¬æãæ¹ããã¹ãç¹
v8 ã® README ã«ãããAPI issue ã§ .generate() ã䜿ããªããšã㯠.generateBeta() ã䜿ãããšããèšè¿°ã¯ã宿
ãšåã£ãŠããªããäž¡è
ã¯è¿ããã®ãéã奿©èœïŒÂ§2.1ïŒã§ãããv9 ã§ã¯ä»¥äžã®ããã«èª¬æããã
- ç»å URL ãæ¬²ãã â
toURL()ïŒ/fakequoteãç»å㯠Voids åŽã«ãã¹ããããŸãïŒ- ç»åããŒã¿ã ãæ¬²ãã â
toBuffer()ïŒ/fakequotebetaã1 åŸåŸ©ã§æžã¿ãç»åã¯ä¿åãããŸããïŒ
| ãªã¹ã¯ | åœ±é¿ | 察ç |
|---|---|---|
| ååã®ãã©ã³ã DL ã 9.6 MB | åå render() ãæ°ç§ããã |
ååã®ã¿ console.info ã§æç€ºããã£ã¹ã¯ãã£ãã·ã¥ã§ 2 åç®ä»¥éãŒããDocker åãã«ãã«ãæããªãã§ããæé ã README ã«èšèŒïŒÂ§8.3ïŒããã軜ã fontSource ã«å·®ãæ¿ãå¯èœ |
| éåç¶²ã»ãªãã©ã€ã³ç°å¢ | ãã©ã³ã DL ã倱æ | 倱æããŠãäŸå€ã«ãã warn + ã·ã¹ãã ãã©ã³ãã§ç¶è¡ãMIQ_FONT_CACHE_DIR ã«äºåé
眮ããã°ãªãã©ã€ã³ã§å®å
šåäœ |
@napi-rs/canvas ã®ãã€ããªããªãç°å¢ |
ã€ã³ã¹ããŒã«ïŒå®è¡æã«å€±æ | dependencies ã«çœ®ã install æç¹ã§æ°ä»ããç¶æ
ã«ãããREADME ã«å¯Ÿå¿ãã©ãããã©ãŒã 衚ãèŒããé察å¿ç°å¢åãã« makeitaquote/api ãæ¡å
ãCI ã§å
š OS + Alpine ãåãïŒÂ§5.10ïŒ |
| Twemoji / Discord CDN ã®å°éæ§ã»ã¬ãŒãå¶é | çµµæåãæ¬ ãã | ky ã®ãªãã©ã€ãLRUãè² ã®ãã£ãã·ã¥ãin-flight åæµã倱ææã¯äŸå€ã«ããå
ããã¹ããæç»ïŒonAssetError: 'text' ãæ¢å®ïŒ |
ky ã® ESM-only ã«ãã CJS ç Žæ |
require() å©çšè
ãå
šæ»
|
tsdown ã® deps.alwaysBundle ã§ãã³ãã«ãscripts/check-build.js ã§ require('./dist/index.cjs') ã®æåã CI ã§ä¿èšŒ |
GlobalFonts.register ã® Buffer åç
§åé¡ |
ãã©ã³ããåããïŒã¯ã©ãã·ã¥ | èªåååŸãã©ã³ãã¯å¿
ããã¡ã€ã«çµç±ã§ registerFromPath()ãBuffer ç»é²æã¯ registry ãåç
§ãä¿æ |
| BudouX ã®åå²ãäžèªç¶ãªã±ãŒã¹ | æ¹è¡äœçœ®ãäžèªç¶ | å質åäžã®äžä¹ããšããäœçœ®ã¥ããphraseBreak: false ã§ç¡å¹åã§ããç¡å¹åããŠãã°ã©ãã§ã å¢çã§å¿
ãæãã |
| dependabot 廿¢ã§äŸåãå€ããªã | è匱æ§ã®èŠèœãšã | ãªãªãŒã¹åã« npm outdated / npm audit ãæå確èªããæé ã CONTRIBUTING ã«èšèŒ |
| Trusted Publishing ã®èšå®æŒã | release ã¯ãŒã¯ãããŒã publish ã§å€±æ |
ãã§ãŒãº 9 ã®æåã®æé ãšã㊠npmjs.com åŽã®èšå®ãæèšïŒÂ§5.11ïŒ |
| TypeScript 6 ã 1 äžä»£åã«ãªã | å°æ¥çãªç§»è¡ã³ã¹ã | TS6 㯠TS7 ãžã®æ©æž¡ããšããŠèšèšãããå®å®çãtsdown 㯠Oxc ããŒã¹ã§åæ€æ»ã« tsc ã䜿ããªãããããã«ã㯠TS ã®ããŒãžã§ã³ã«çžãããªã |
| Voids API ã®æä¹ åæ¢ | makeitaquote/api ãç¡äŸ¡å€å |
æ¬äœïŒããŒã«ã«çæïŒãäž»æ©èœãªã®ã§èŽåœå·ã«ãªããªããããã v9 ã®æå€§ã®çã |
| äž¡ãšã³ããã€ã³ãã®ç»åãã¶ã€ã³å·® | ç§»è¡æã«èŠãç®ãå€ããå¯èœæ§ | ãã§ãŒãº 3 ã§å®åºåãæ¯èŒããå·®ãããã° README ã«æ³šèšïŒÂ§2.1ïŒ |
åçå®è£ åŸãå®å©çšã®ã¹ã¯ãªãŒã³ã·ã§ãããããšã«è¿œå ããã
Theme.layout: 'side' | 'new' ãå°å
¥ããã¬ã€ã¢ãŠãèšç®ã src/render/layout.ts ã«éçŽããã
side |
new |
|
|---|---|---|
| ã¢ãã¿ãŒ | å¹
widthRatioãå·Šå³ã©ã¡ãã |
ãã£ã³ãã¹å šé¢ |
| ã°ã©ããŒã·ã§ã³ | æ°Žå¹³ïŒå³é 眮æã¯èªåãã©ãŒïŒ | åçŽïŒäžâäžïŒ |
| åŒçšç¬Š | ã€ã³ã©ã€ã³ | ãããã¯ïŒå€§ããäžéšã«ïŒ |
| åºåãç· | ç¡å¹ | æå¹ |
| æ¢å®ãµã€ãº | 1280Ã720 | 800Ã1000 |
ããªã»ããã« new ã¬ã€ã¢ãŠãã® dark/light çã远å ïŒèš 5 çš®ïŒã new ã¯çžŠé·å°çšã§ã¯ãªããæšªé·ãã£ã³ãã¹ã«ãé©çšã§ããã
å転ïŒããã¹ãå·Šã»ã¢ãã¿ãŒå³ïŒã avatar.position ã®æå®ã ãã§æç«ãããããã text.area ã®æ¢å®å€ã 'auto' ãšããã¬ã€ã¢ãŠããš position ããå°åºããããã«ããã
side: width = 1 - widthRatio - gap * 2
x = position === 'right' ? gap : widthRatio + gap
new: { x: 0.08, y: 0.56, width: 0.84, height: 0.18 }
æ¢å®å€ïŒwidthRatio 0.5, gap 0.04ïŒã¯åŸæ¥ããŒãã³ãŒãããŠããå€ïŒx 0.54 / width 0.42ïŒãšäžèŽãããããæ¢åããŒãã®èŠãç®ã¯å€ãããªããwidthRatio ãå€ãããšãã«ããã¹ãã远éããããã«ãªã£ãããšãã坿¬¡å¹æãããã
watermark.position ã«ã 'auto' ã远å ãããããæ¢å®ãšãããside ã§ã¯ã¢ãã¿ãŒã®å察åŽã«çœ®ããå転æã«ãŠã©ãŒã¿ãŒããŒã¯ãã¢ãã¿ãŒã«åãããåé¡ãžã®å¯ŸåŠã
ctx.font ã« weight ãæå®ããŠãå¹ããªãããšã宿ž¬ã§å€æããã
normal 40px Noto Sans JP width=120.36
bold bold 40px Noto Sans JP width=120.36
900 900 40px Noto Sans JP width=120.36
300 300 40px Noto Sans JP width=120.36
åå ã¯ãGlobalFonts ã«ç»é²ããå¯å€ãã©ã³ãïŒNotoSansJP[wght].ttfïŒã®æ¢å®ã€ã³ã¹ã¿ã³ã¹ïŒwght=400ïŒãã Skia ã«èŠããŠããªãããšãgoogle/fonts ã«éçã€ã³ã¹ã¿ã³ã¹ã¯ååšããªãïŒ404 ãç¢ºèªæžã¿ã§8.2ïŒãã€ãŸãèªåååŸãã©ã³ãã䜿ãéããweight æå®ã¯åžžã«ç¡èŠãããã
察åŠ: åæå€ªåïŒsrc/render/textStyle.tsïŒ
- ãã¡ããªããšã« 1 åã ãã
100pxãšbold 100pxã®measureTextãæ¯èŒããŠãæ¬ç©ã® bold face ããããããæ€åºããçµæããã£ãã·ã¥ããã - æ¬ç©ãããã°
ctx.fontã® weight ããã®ãŸãŸå¹ãã®ã§ãäœãããªãã - ãªããã°
strokeTextã fill ãšåè²ã»fontSize * 0.045 * heavinessã®ç·å¹ ã§éããŠããfillTextããã°ãªãã倪ãããã
æ°å€ weight 㯠(weight - 500) / 400 ã§ç·å¹
ã«ãããã³ã°ããããã600 ãš 900 ã§å€ªããå€ãããå©çšè
ãã·ã¹ãã ã«æ¬ç©ã® Bold ãæã€ãã©ã³ããç»é²ããå Žåã¯ããã¡ããåªå
ãããã
ctx.save()/ctx.restore()ãstrokeStyleã埩å ããªãããšã宿ž¬ã§å€æãããããfillTextã§ã¯æç€ºçã«éé¿ã»åŸ©å ããŠããã
text.quotesã廿¢ããquoteMark: { display, chars, size, color, weight, gap }ã«çµ±åãdisplay: 'block'ã瞊é·ã¬ã€ã¢ãŠãã®å€§ããªåŒçšç¬Šã«ããããdivider: { enabled, widthRatio, thickness, color, gap }ã远å ïŒåŒçšæãšçœ²åã®éã®çœ«ç·ïŒã- ãã¹ãŠã®ããã¹ãèŠçŽ ã«
weightã远å ã - ãã¹ããå®éã« Twemoji / Discord CDN ãžã¢ã¯ã»ã¹ããŠããã®ãä¿®æ£ã
pipeline.test.tsã§fetchãã¹ã¿ãåããããããã¯ãŒã¯é å»¶ã§ 5 ç§ã¿ã€ã ã¢ãŠãããäžå®å®ãªãã¹ãã§ããã§10.1 ã«æžããããã¹ãã¯ãããã¯ãŒã¯ã«è§Šããªããæ¹éã«ãåããŠããã
§13 ã«ãã°ã©ããŒã·ã§ã³ãæ°Žå¹³åºå®ãªã®ã§çžŠé·ã¬ã€ã¢ãŠããç Žç¶»ããããšèšé²ããŠãããã gradient.direction ã®è¿œå ã«ããè§£æ¶ããã
§8.2 ã®ãjsDelivr ã®å¯å€ãã©ã³ã URL ãããŒãã³ãŒããæ¹åŒã廿¢ããGoogle Fonts CSS API ã§æ¯å解決ããæ¹åŒã«å€æŽããã
GET https://fonts.googleapis.com/css2?family=Dela+Gothic+One
â @font-face { src: url(https://fonts.gstatic.com/s/delagothicone/v19/âŠ.ttf) }
åŸãããå©ç¹:
| æ§ïŒjsDelivr åºå® URLïŒ | æ°ïŒCSS APIïŒ | |
|---|---|---|
| Noto Sans JP ã®ãµã€ãº | 9.6 MBïŒå¯å€ãã©ã³ãå šãŠã§ã€ãïŒ | 2.94 MBïŒéç RegularïŒ |
| ããŒãžã§ã³ | @main åºå®ãæŽæ°ã«è¿œéããªã |
URL ã« v56 çãå
¥ãåžžã«ææ° |
| 察å¿ãã©ã³ã | 2 çš®ãããŒãã³ãŒã | Google Fonts å šãã¡ã㪠|
| 倪å | å¯å€ãã©ã³ãã®å¶çŽã§äžå¯ïŒÂ§14.3ïŒ | weights: [400, 700] ã§æ¬ç©ã® Bold |
ã©ã€ã»ã³ã¹æ¹é: Google Fonts 㯠SIL OFL / Apache 2.0 / UFL ã®ã¿ãé
åžããŠããããã ãCSS API ã§è§£æ±ºã§ãã = èªç±ã©ã€ã»ã³ã¹ããæãç«ã€ãæåã»ã©ã€ã»ã³ã¹äžæã®ãã©ã³ã㯠API ã 400 ãè¿ãã®ã§ãååæå®ã§ã¯ååŸã§ããªããä»ã®é
åžå
ãžãã©ãŒã«ããã¯ããå®è£
ã¯æå³çã«æããªããå©çšè
ãèªåã§ã©ã€ã»ã³ã¹ãåŸããã©ã³ã㯠registerFromPath() / registerFromURL() ã§æç€ºçã«èªã¿èŸŒããã
æå®ããã 19 çš®ã®ãã¡ 17 çš®ãå©çšå¯èœã2 çš®ãäžå¯ïŒå®æž¬ïŒ:
Jiyu no Tsubasaâ Google Fonts ã«ãªããã©ã€ã»ã³ã¹äžæCastor Titlingâ Google Fonts ã«ãªãDacing Scriptã¯Dancing Scriptã®ã¿ã€ãïŒSUGGESTIONSã§èªå°ïŒ
catalogue.ts ã«æ¢ç¥ã®äžå¯ãã©ã³ããš typo ãæããã400 ã®ãšãã«ããªãåããªãããã代ããã«äœããã¹ããããå«ãã¡ãã»ãŒãžãåºãã
online: false: AutoFontOptions.onlineïŒenabled ãå¥åãšããŠç¶æïŒã§ãããã¯ãŒã¯ã¢ã¯ã»ã¹ãå®å
šã«ç¡å¹åã§ããããã£ã¹ã¯ãã£ãã·ã¥ãšã·ã¹ãã ãã©ã³ãã®ã¿ã䜿ãã
text.font ã 'Dela Gothic One, Noto Sans JP, sans-serif' ã®ãšãã å
é ã®ãã©ã³ããããŠã³ããŒããããªããã°ããã£ãã
åå : prepareFonts ã resolveFamily(request) === null ã§ã解決å¯èœãããå€å®ããŠãããããã¹ã¿ãã¯åŸæ¹ã® Noto Sans JP ãæ¢ã«ç»é²æžã¿ã ãšã解決ã§ããããšèŠãªããŠæ©æ return ããŠããã
ä¿®æ£: ensureStack() ã远å ããã¹ã¿ãã¯ãå
é ãã蟿ã£ãŠæåã«äœ¿ãããã®ãèŠã€ãããŸã§ ååŸã詊ã¿ããå
é ãåããã°åŸç¶ã¯ååŸããªãïŒç¡é§ãªããŠã³ããŒããé¿ããïŒã
:name: / :name@host: / :name@.: ã https://{host}/emoji/{name}.webp ã«è§£æ±ºããã
ãªããã€ã³å¿
é ãšããã:name: ã¯éåžžããã¹ãã§é »åºããç¹ã« 12:30:45 ã® :30: ã誀çãããMiQOptions.misskey ã«ã€ã³ã¹ã¿ã³ã¹ãèšå®ããããšãã ãè§£éããã
å ããŠæ£èŠè¡šçŸã« (?<![A-Za-z0-9_]) ã®åŸèªã¿ãå
¥ããASCII è±æ°åçŽåŸã®ã³ãã³ãé€å€ãããããã§ 12:30:45 ã http:// ã匟ãããããã¹ã:emoji:ïŒæ¥æ¬èªçŽåŸïŒã¯éãã
| èšæ³ | 解決å |
|---|---|
:name: |
èšå®ãããã€ã³ã¹ã¿ã³ã¹ |
:name@host: |
hostïŒremote: false ã§ç¡å¹åå¯ïŒ |
:name@.: |
èšå®ãããã€ã³ã¹ã¿ã³ã¹ïŒMisskey ã®é£å衚èšïŒ |
quoteMark.displayã®æ¢å®ã'inline'â'none'ãåŒçšç¬Šãªããæšæºãnewããªã»ããã®ã¿'block'ãavatar.shapeïŒcircle/roundedïŒãåé€ãäžèŠãšã®å€æãclipToShape()ãšãã®åŒã³åºããåé€ããã
105 ã±ãŒã¹ã¯å€ãããŠå·®åãèªã¿åããªãã£ãããã71 ã±ãŒã¹ã»11 ã°ã«ãŒãã«æŽçããã°ã«ãŒãããšã®ãã©ã«ãã«åºåããããã«ããã
.visual/
âââ 01-themes/ 05-typography/ 09-sizes/
âââ 02-layout/ 06-fonts/ 10-formats/
âââ 03-text/ 07-quotes/ 11-discord/
âââ 04-emoji/ 08-avatar/
âââ index.html
éžå®åºæºã¯ãåãã°ã«ãŒãå
ã®ä»ã±ãŒã¹ãšèŠãç®ãæç¢ºã«éãããšããã»ãŒåãçµµã«ãªãããªãšãŒã·ã§ã³ïŒweight 300/600ãé¿ãããã©ãŒãããå·®ãªã©ïŒã¯ãŠããããã¹ãåŽã«ä»»ããŠé€å€ããã06-fonts ã¯ã«ã¿ãã° 17 çš®ãèªåå±éããã
çµµæåã°ã«ãŒã㯠assets/discordemoji.json ãš assets/misskeycustomemoji.json ã® å®ããŒã¿ã䜿ãããã3 çš®ã®çµµæåãå®éã« CDN ããååŸã§ããããšãç®èŠç¢ºèªã§ããã
ååŸã«å€±æããçµµæåã§æçµè¡ããã£ã³ãã¹å€ã«ã¯ã¿åºããã°ããã£ãã
åå : ã¬ã€ã¢ãŠãã¯çµµæåã fontSize ã®æ£æ¹åœ¢ãšããŠæž¬ãããæç»æã¯ç»åãç¡ããš onAssetError: 'text' ã«ãããœãŒã¹æååã«ãã©ãŒã«ããã¯ããã <:nope:123456789012345678> 㯠26 æåãããæ£æ¹åœ¢ 1 ååãšã¯å¹
ãæ¡éãã«éãã
察åŠ: resolveEmojiSegments() ã远å ããã¬ã€ã¢ãŠãåã«ååŸå€±æããçµµæåãããã¹ãã»ã°ã¡ã³ããžå€æãããããã§æž¬å®ãšæç»ãåã察象ãèŠãããã«ãªããããã«å¯æ¬¡å¹æãšããŠãé·ããããã©ãŒã«ããã¯æååãéåžžã®ããã¹ããšåæ§ã«æãè¿ããããã«ãªã£ãïŒçµµæåããŒã¯ã³ã¯åå²äžèœãªã®ã§ã以åã¯æž¬å®ãçŽããŠãã¯ã¿åºãããŸãŸã ã£ãïŒã
src/theme/color.ts ã远å ãå
šãŠã®è²ãã£ãŒã«ãã ColorInput ãåããã
| èšæ³ | äŸ |
|---|---|
| 16é² | #RGB #RGBA #RRGGBB #RRGGBBAA |
| æ°å€ | 0xRRGGBB 0xRRGGBBAA |
| é å | [r, g, b] [r, g, b, a]ïŒalpha 㯠0â1 / 0â255 䞡察å¿ïŒ |
| ããŒã¯ãŒã | 'transparent' |
| CSS 颿° | rgb() rgba()ïŒ50% 圢åŒã® alpha ãå¯ïŒ |
æ°å€è¡šèšã®åççå¶çŽ: 0x00FF0000 ã¯æ°å€ãšã㊠0xFF0000 ãšå®å
šã«åäžã§ãããå
é ãŒããã€ããæã€è²ã¯æ°å€ã§ã¯è¡šçŸã§ããªããå€å®ã¯å€§å°ã§è¡ãïŒ> 0xFFFFFF ãªã alpha ãã€ãä»ãïŒã0xFF0000 ã¯ãäžéæãªèµ€ããšè§£éãããé·ããå€ã®äžéšãšãªãæåå衚èšã䜿ãã°ææ§ãã¯ãªãããã®å¶çŽã¯åã® doc ãšãã¹ãã«æèšããã
custom ããªã»ãã: èæ¯ã»æ¬æã»çœ²åã»ãŠã©ãŒã¿ãŒããŒã¯ããã¹ãŠ transparent ã«ããããŒããå®å
šéæã®èŠçŽ ã¯æç»èªäœãã¹ãããããã®ã§ãextends: 'custom' ã®äžã«çœ®ããè²ã ããç»åã«çŸãããèæ¯ãæå®ããªããã°éé PNG ãåŸãããã
- æ¢å®ãµã€ãºãé·èŸº 800 ã«ïŒæšªé· 1422Ã800 / çžŠé· 800Ã1000ïŒã
setScale(factor)ã远å ãããŒãå ã®å¯žæ³ã¯ãã¹ãŠãã£ã³ãã¹ã«å¯Ÿããæ¯çãªã®ã§ãããã¯çã®ãºãŒã ã«ãªãïŒã¬ã€ã¢ãŠãã¯äžåå€ãããè§£å床ã ãå€ããïŒãäžé 8ãsetSize()ãéæšå¥šåãã¢ã¹ãã¯ãæ¯ã ããå€ããŠä»ãåãããªããããã¢ãã¿ãŒã»ã°ã©ããŒã·ã§ã³ã»ããã¹ãã®æ¯çã厩ãããdiscord.js ã«å£ãprocess.emitWarning(..., { type: 'DeprecationWarning', code })ã§ 1 åã ãèŠåããïŒ--no-deprecationã§æå¶ã§ããconsole.warnãšéãæšæºçãªå¶åŸ¡ãå¹ãïŒã圢ç¶ãå€ãããå Žåã¯setTheme({ width, height })ãæ£ããå ¥å£ãsizeToAvatar: 'width' | 'height'ã远å ãã¢ãã¿ãŒãåå¯žã§æãããããã«ãã£ã³ãã¹å šäœã 1 ã€ã®ä¿æ°ã§ã¹ã±ãŒã«ãããã¢ã¹ãã¯ãæ¯ã¯ä¿ãããã®ã§æªãŸãªããavatar.fit: 'cover' | 'contain'ã远å ã
ã©ãã³å°çšãã©ã³ããæå®ãããšæ¥æ¬èªãè±è ã«ãªãåé¡ã
æåã®å®è£
ã¯èª€ãã ã£ãã ããã©ã€ããªãåœè©²å圢ãæã€ããã measureText ã®å¹
æ¯èŒã§å€å®ããããšããããå圢ãæããªããã©ã³ãã®å®æž¬å¹
㯠tofu ãšäžèŽããªã ïŒVina Sans ã§ ç« ãæž¬ããš monospace ãšã¯éãå€ãè¿ããå®éã«ã¯ç¡é¢ä¿ãªå圢ãæãããïŒããããŒãã¯åžžã«ãã«ããŒããŠããããšèª€çããã
宿ž¬ã§åãã£ãæ£ããäºå®:
Vina Sans ç«=81.0 A=41.0
Vina Sans, Noto Sans JP ç«=100.0 A=41.0 â Noto ã®å¹
/ Vina ã®å¹
Noto Sans JP ç«=100.0 A=57.4
Skia ã¯ãã©ã³ãã¹ã¿ãã¯ãå圢åäœã§è§£æ±ºããã ã€ãŸãã¹ã¿ãã¯ã宣èšããã ãã§æ£ããåããå€å®ã¯äžèŠãã€æå®³ã ã£ãã®ã§æ€å»ããneedsGlyphFallback(text) ãéã©ãã³æåã®ååšã ããèŠãŠããã©ãŒã«ããã¯ãç¡æ¡ä»¶ã«é£çµããæ¹åŒã«ãããäžèŠãªãšãã®ã³ã¹ãã¯ãŒãã§ãå¿
èŠãªãšãã¯ç¢ºå®ã«å¹ãã
setFromMessage(message, { avatar, name })ãæ¢å®ã¯äž¡æ¹ãµãŒããŒåŽïŒ'guild'/'nickname'ïŒã§ããã®æ¹ãããã®ãµãŒããŒã®èªã¿æãèŠããã®ãã«äžèŽããã'global'ã§ã¢ã«ãŠã³ãåŽã«åãæ¿ãããã©ã¡ããéžãã§ã仿¹ããã©ãŒã«ããã¯ã«ãªãã- discord.js ã®
member.displayNameã¯ãããã¯ããŒã ãç¡ããã°ã°ããŒãã«åããªã®ã§ãããã¯ããŒã å€å®ã§ã¯member.nicknameãå ã«èŠãïŒããããªããš'global'æå®æã«ã°ããŒãã«åãèªåèªèº«ã«é ãããïŒã misskeyãæååã»é åã»ãªãã·ã§ã³ãªããžã§ã¯ããåãããè€æ°ã€ã³ã¹ã¿ã³ã¹æå®æãçŽ ã®:name:ã¯ã©ãã«å±ãããåãããªããããã»ã°ã¡ã³ãã«alternativeUrlsãæãããããŒããŒãé ã«è©ŠããŠæåã«æåãããã®ã䜿ãã
æ¢å®ã M PLUS Rounded 1cããã®èåŸã« Noto Sans JP ãšãããåè ã¯ãã®çšéã«åãäžžãŽã·ãã¯ã§ã©ãã³ãã«ããŒããåŸè ã¯ã«ãã¬ããžãåºãã®ã§åãããŒãã®åãç¿ã«ãªãã§16.4 ã®éãã¹ã¿ãã¯ã¯å圢åäœã§è§£æ±ºãããããããã® 2 段æ§ãã¯å®éã«æ©èœããã
.visual/ïŒgitignoreïŒã docs/visual/ïŒã³ãããå¯Ÿè±¡ïŒ ã«ç§»ãã DESIGN.md ã¯ãªããžããªã«ãŒããžç§»åããã
scripts/visual-check.jsã¯ç»åãšmanifest.jsonïŒãã¡ã€ã«åãªã¹ããšã¡ã¿æ å ±ïŒãåºåããdocs/ã¯çŽ ã® HTML / CSS / JS 1 ãã¡ã€ã«ã®éçãµã€ãã§ãmanifest ãèªãã§äžèЧãçµãããã«ãäžèŠã§ãæç»åŽã«è§Šããã«èŠãæ¹ã ã倿Žã§ãã- GitHub Pages ã®ãœãŒã¹ã¯
mainãã©ã³ãã®/docsãActions ã§ã®ãããã€ã¯æ¡ããªãã£ã:docs/ã Pages 察象ã«ãããšããèŠä»¶ã«ã¯ãã©ã³ãé ä¿¡ãçŽ çŽã«å¯Ÿå¿ããenvironment/pages: write/ OIDC ãäžåäžèŠã«ãªã - ãªãªãŒã¹ã¯ãŒã¯ãããŒã¯ç¬ç«ãããžã§ãã§ã®ã£ã©ãªãŒãåçæã
mainã«ã³ãããããããã©ã³ãã»çµµæå CDN ã䜿ãããããã®äžèª¿ã publish ãæ¢ããªããã release ã®åŸæ®µã«çœ®ãã
ç»åãµã€ãº: å š 77 ã±ãŒã¹ããã«è§£å床 PNG ã§åºããš 19MB ã«ãªããã³ãããããææç©ãšããŠé倧ã ã£ããæ€èšŒã¯ãã«è§£å床ã®çãã€ãã«å¯ŸããŠè¡ããæžãåºãæ¹ã ãé·èŸº 900px ã® WebP ã«çž®å°ããŠããïŒ1.9MBïŒãæ€èšŒã®å³å¯ãã¯èœãšãããªããžããªã ã軜ããªãã
æèš: å ¬éç©ãªã®ã§ãåãåãããŠåäœã§ç®ã«ãããŠãåé¡ãªãæç« ã®ã¿ã䜿ãããããªãã¯ãã¡ã€ã³ïŒå€ç®æŒ±ç³ãåŸèŒ©ã¯ç«ã§ãããïŒãšã©ã€ãã©ãªèªäœã«ã€ããŠã®äžç«çãªèšè¿°ã§ãå®åšã®äººç©ã®çºèšãšèªãããã®ã¯çœ®ããªãã
å®è£ ïŒ2026-07-31 å®äºïŒã§ãèšèšæã®æ³å®ãšå®éãé£ãéã£ãç¹ãããããå®è£ åŽãæ£ãšããæ¬æžã远éãããŠããã
| # | èšèšæã®æ³å® | å®é | å¯Ÿå¿ |
|---|---|---|---|
| 1 | tsdown 㯠deps: { external, noExternal } |
0.22.14 㯠neverBundle / alwaysBundleãæ§åã¯åãšã©ãŒ |
§5.6 ãæ¹ååŸã®ååã«ä¿®æ£ |
| 2 | BudouX ã«ã¿ã€èªã¢ãã« thModel ããã |
export ãããŠããã®ã¯ jaModel / zhHansModel / zhHantModel ã®ã¿ïŒã¿ã€èªã¯ loadDefaultThaiParser çµç±ïŒ |
PhraseLocale ãã 'th' ãåé€ |
| 3 | ky ã® HTTPError ãã response.clone().text() ã§ããã£ãèªãã |
ky v2 㯠HTTPError çææã«ããã£ãæ¶è²»æžã¿ãããã error ã«ä»ã response ã¯ããã¯ãèŠããã®ãšå¥ã€ã³ã¹ã¿ã³ã¹ãªã®ã§ WeakMap ã§ã远ããªã |
afterResponse ããã¯ããªã¯ãšã¹ãåäœã§æž¡ããã¯ããŒãžã£ã«éé¿ãã captureErrorBody() ãå®è£
ïŒÂ§6.4 ã®å®è£
è©³çŽ°ïŒ |
| 4 | ky v2 ã®ããã¯åŒæ°ã¯ (request, options, response) |
åäžãªããžã§ã¯ã { request, options, response, retryCount } |
å®è£ ã§å¯Ÿå¿ |
| 5 | throwHttpErrors: false ã«ããã°ããã£ãèªããŠãªãã©ã€ãå¹ã |
ky v2 ã§ã¯ ãªãã©ã€ãç¡å¹åãããïŒå®æž¬ïŒè©Šè¡ 1 åã®ã¿ïŒ | æ¡ããã#3 ã®æ¹åŒã« |
| 6 | GlobalFonts.registerFromPath 㯠boolean ãè¿ã |
FontKey | null ãè¿ã |
Boolean() ã§æ£èŠå |
| 7 | Canvas#encode ã¯å
šåœ¢åŒã§ (format, quality) |
AVIF ã®ã¿ (format, AvifConfig) |
encode.ts ã§åå² |
| 8 | 2 ãšã³ããªãªãç¬ç«ãããã³ãã«ã«ãªã | tsdown ãå
±æãã£ã³ã¯ïŒdist/source-*.{cjs,mjs}ïŒãçæãã |
宿ž¬ã§ canvas / budoux ã¯å«ãŸãã tree shaking ãå¹ããŠãããcheck-build.js ãããšã³ããªããå°éå¯èœãªå
šãã£ã³ã¯ã蟿ããïŒãå®éã« require ã㊠require.cache ãèŠããæ€æ»ã«åŒ·åïŒÂ§5.8ïŒ |
| 9 | ã°ã©ããŒã·ã§ã³æ¢å®ã¯ startRatio 0.30 / endRatio 0.62 |
ã¢ãã¿ãŒå³ç«¯ïŒ0.5WïŒã§ã®ã¢ã«ãã¡ã 0.88 çšåºŠã«ãããªãããã¿ã€ã«å¢çã瞊ç·ãšããŠèŠããïŒç¹ã« lightïŒ |
æ¢å®ã 0.22 / 0.5 ã«å€æŽããã¢ãã¿ãŒå³ç«¯ã§ã¡ããã©ã¢ã«ãã¡ 1 ã«å°éããã |
| 10 | ãã©ãŒã«ããã¯ã¢ãã¿ãŒã®ã€ãã·ã£ã«ã¯ç蟺㮠0.4 | å®åºåã§å·šå€§ããã | 0.22 ã«å€æŽ |
| 11 | fonts.ensureDefaults() / registerFromURL() ã registry ã«çœ®ã |
registry â autoload ã埪ç°ãã | src/font/index.ts ã§åæããŠå
¬éïŒÂ§8.3 ã® API 圢ç¶ã¯èšèšã©ããïŒ |
| 12 | test/{unit,integration}/ ã«é
眮 |
ããŒã¹ãã³ãã¬ãŒãæºæ ã§ src/**/*.test.ts ã«äœµçœ® |
§4.3 ã®ãšããå®è£ |
èšèšã©ããã«æ©èœããããšã®ç¢ºèª
makeitaquote/apiãrequire()ããŠãnode_modulesããäœãããŒããããªãïŒkyã¯ãã³ãã«æžã¿ãcanvas ã¯å°éäžèœïŒãcheck-build.jsã® 46 æ€æ»ã§æ ä¿ã- ãã©ã³ãèªåååŸãå®éã«åäœããèšå®ãŒãã§æ¥æ¬èªãæç»ã§ããããšãå®åºåã§ç¢ºèªã
- BudouX ã«ããæç¯æ¹è¡ãšçŠååŠçãå®åºåã§æ©èœïŒ
ããè¡é ã«æ¥ãªããæç¯å¢çã§æããïŒã - å š 174 ãã¹ãã»åæ€æ»ã»Biomeã»ãã«ãæ€æ»ãã°ãªãŒã³ã
ãã¹ãŠ 2026-07-31 æç¹ã§ç¢ºèªã
- çŸè¡
index.jsã®generate/generateBetaå®è£ ïŒãšã³ããã€ã³ãã®æ©èœå·®ã§2.1ïŒ examples/miq-with-discordjs.md/examples/beta-with-discordjs.mdïŒURL è¿ä¿¡ vs æ·»ä»ãšãã䜿ãåãã®è£ã¥ãïŒoto-lab/npm-biome-tsã®å šæ§æãã¡ã€ã«ïŒpackage.json/biome.json/tsconfig.json/tsdown.config.ts/vitest.config.ts/.gitattributes/.gitignore/.github/workflows/{ci,release}.yml/scripts/setup.jsïŒ@napi-rs/canvas@1.0.3ã®optionalDependenciesïŒããªãã«ã察象ãã©ãããã©ãŒã ïŒnode-canvas-with-twemoji-and-discord-emoji@1.2.2ã®src/drawTextWithTwemoji.js/src/utils/splitEntitiesFromText.js/src/utils/loadTwemojiImageByUrl.jsïŒã»ã°ã¡ã³ãåã»çµµæåé 眮ã»ãã£ãã·ã¥ã®ã¢ã«ãŽãªãºã ãcanvasãžã®çŽæ¥äŸåãmaxWidthã®å®è£ ïŒ@miq4d/canvas-with-discord-content@2.1.2ã®package.jsonïŒäŸåããŒãžã§ã³ïŒkuromoji@0.1.2/kuroshiro@1.2.0ã®time.modifiedãšdist.unpackedSizeïŒ41.3 MBã2022 å¹Žã§æŽæ°åæ¢ïŒbudoux@0.8.4ã® README ãšdist.unpackedSizeïŒParser+jaModelã®åæ APIïŒky@2.0.2ã®type/exportsïŒESM-onlyïŒtypescriptã®dist-tagsïŒlatest: 7.0.2ã6 ç³»ã®ææ°ã¯6.0.3ïŒtsdown@0.22.14/vitest@4.1.10/@biomejs/biome@2.5.6ã®ææ°ç- jsDelivr äžã® Noto ãã©ã³ãã®å®åšã»ãµã€ãºç¢ºèªïŒ
curl -IïŒNotoSansJP 9,589,900 bytes / NotoSans 2,049,096 bytesãstatic ç㯠404ïŒ - Brooooooklyn/canvas#1006ïŒ
GlobalFonts.registerã® Buffer åç §åé¡ïŒ - tsdown: Migrate from tsupïŒèšå®é
ç®ã®å¯Ÿå¿ããã ãå®éã®
DepsConfig㯠§14-1 ã®ãšããæ¹åãããŠããïŒ tsdown@0.22.14ã®dist/types-*.d.mtsã«ããDepsConfigïŒneverBundle/alwaysBundleïŒky@2.0.2ã®ããã¯åŒæ°ãšãšã©ãŒæã®ã¬ã¹ãã³ã¹æ¶è²»æåïŒå®æž¬ã§14-3ã5ïŒ
makeitaquote/api ãµããã¹ã åé€ããVoids API ã¯ã©ã€ã¢ã³ããå¥ãªããžããªã»å¥ããã±ãŒãž @makeitaquote/voids ã«åãåºããmakeitaquote ã¯ããŒã«ã«æç»ïŒïŒ miq CLIïŒã ããæã€ããã±ãŒãžã«ãªãã
require('makeitaquote') â ããŒã«ã«ç»åçæïŒå¯äžã®ãšã³ããªïŒ
require('@makeitaquote/voids') â Voids API ã¯ã©ã€ã¢ã³ãïŒå¥ããã±ãŒãžïŒ
§4.1 ã§ããµããã¹å㯠voids ã§ã¯ãªã apiããšãã倿ã¯ãå°æ¥ Voids 以å€ã® API å®è£
ãè¶³ããšããåæã«ç«ã£ãŠããã3 幎éçšããŠãã®å®è£
㯠1 ã€ãå¢ãããåæãæç«ããªãã£ããååã®æœè±¡åºŠã ããæ®ããäžèº«ã¯æåããæåŸãŸã§ Voids å°çšã ã£ãã
| # | çç± | å 容 |
|---|---|---|
| 1 | å ±æããŠãããã®ãå°ãªã | src/api/* ãæ¬äœãšå
±æããŠããã®ã¯å
¥åæ£èŠåïŒcore/quoteïŒã»MessageLike ç³»ã®åã»http/client ã ããæç»ã»ãã©ã³ãã»çµµæåã»ããŒãã»åºåãšã¯äžåã®æ¥ç¹ããªã |
| 2 | å£ãæ¹ãéã | ããŒã«ã«åŽã¯ãã€ãã£ããã€ããªã®æç¡ã§å£ããAPI åŽã¯ä»äººã®ãµãŒãã®æ»æŽ»ã§å£ãããåå± ããããš Issue ãæ¯åãã©ã¡ãã®ãšã³ããªããã®åãåãããå§ãŸãïŒÂ§5.12 ã® Issue ãã³ãã¬ãŒãã«å®éã«ãã®èšåããã£ãïŒ |
| 3 | ãªãªãŒã¹ãé£åããŠããŸã | æç»åŽã®ä¿®æ£ãåºããã³ãç¡é¢ä¿ãª HTTP ã¯ã©ã€ã¢ã³ããå publish ããŠãããéã« API åŽã® URL 倿Žã²ãšã€ã§æ¬äœã®çãäžãã |
| 4 | éçšäž»äœãéã | Voids API ã¯æ¬ããã±ãŒãžã®éçºè ã®éçšç©ã§ã¯ãªããåãããã±ãŒãžã«å梱ãããšããã®è²¬ä»»åçã README ã®æ³šèšã§ãã衚çŸã§ããªã |
| 5 | ãã«ãå¢çã®ç¶æã³ã¹ã | ãmakeitaquote/api ãããã€ãã£ãã¹ã¿ãã¯ãžå°éããªããä¿èšŒã®ããã« check-build.js ã®æ€æ» 4ã»5 ãš RENDERING_DEPS ãç¶æããŠãããããã±ãŒãžãåããã°ããã®äžå€æ¡ä»¶ã¯ãããããäŸåããŠããªãããšããèªæãªäºå®ã«ãªã |
çç± 5 ãå®è³ªçã«äžçªå€§ããã§17 ã®ãèšèšã©ããã«æ©èœããããšã®ç¢ºèªãã«æãã£ãŠãã makeitaquote/api ã®åé¢ä¿èšŒã¯ãããã±ãŒãžã 1 ã€ã§ããããšã®åž³å°»ãåãããããã®ä»æãã§ãããåé¢ããã°æ€æ»ããšäžèŠã«ãªãã
| 察象 | å 容 |
|---|---|
src/api/ |
client.ts / client.test.ts / endpoints.ts / types.ts / index.ts ã® 5 ãã¡ã€ã« |
examples/api.ts |
ãµããã¹ã®äœ¿çšäŸ |
package.json |
exports['./api']ãkeywords ã® "api"ãdescription ã® "or via an external API" |
tsdown.config.ts |
entry ãã src/api/index.ts |
scripts/check-build.js |
RENDERING_DEPSãæ€æ» 4ïŒrequire æã®ããŒãå
容ïŒãæ€æ» 5ïŒå°éå¯èœãã£ã³ã¯ã®éçæ€æ»ïŒãæ€æ» 1 ã® api/* ãã¹ã以éã®çªå·ãç¹°ãäžã |
src/http/client.ts |
post() ãš RequestOptions.jsonãVoids ã¯ã©ã€ã¢ã³ãå°çšã§ãä»ã®åŒã³åºãå
㯠get / head / getBuffer ãã䜿ã£ãŠããªã |
src/http/client.ts èªäœã¯æ®ããçµµæåã»ãã©ã³ãã»ã¢ãã¿ãŒã»miq env ã® 4 系統ã䜿ã£ãŠããã
src/theme/presets.tsãšsrc/core/sizing.test.tsã«ããapi.voids.topãžã®èšåãããã¯ã¬ã€ã¢ãŠã宿°ã®åºèªãèšé²ããã³ã¡ã³ãã§ãããã®å€ãå®éã« API ãå©ããŠç¢ºèªããããšããäºå®ã¯åé¢åŸãå€ãããªãDESIGN.md㮠§2.1 / §4 / §6.4 ãªã©ãv9 åœæã®èšèšèšé²ã廿¢ã®æ³šèšãä»ããããã§æ®ã
ç§»èšå
ã§ã¯ååãšæåãå€ããªããVoidsMiQïŒããã³ MiQ å¥åïŒã»VoidsOptions / VoidsPayload / VoidsQuoteDataã»VoidsApiErrorã»endpoints / DEFAULT_BASE_URL ã¯ãã¹ãŠåããå©çšè
ã®å·®åã import æ 1 è¡ã§æžãããã«ããããã
æ¬äœãšå
±æããŠããã³ãŒãïŒå
¥åæ£èŠåã»MessageLike ç³»ã®åã»http/clientïŒã¯ãmakeitaquote ã«äŸåããã®ã§ã¯ãªãç§»èšå
ãžã³ããŒãããäŸåããããšãAPI ã ã䜿ããã人ã«ãã€ãã£ããã€ããªãåŒãããªãããšãã §4.2 ã®ç®çãæ¯ãåºãã«æ»ããããæ£èŠåããžãã¯ã¯ 100 è¡ã«æºããªãã
æ§ç¯æé 㯠.private/stack/*.md ã«çœ®ãã
| äœ¿ãæ¹ | v12 ã§ã®åœ±é¿ |
|---|---|
import { MiQ } from 'makeitaquote' |
ãªã |
miq CLI |
ãªã |
import { VoidsMiQ } from 'makeitaquote/api' |
npm i @makeitaquote/voids ã㊠import å
ãå·®ãæ¿ããããã以å€ã®å€æŽãªã |
VoidsApiError ã root ãã catch |
root ããã¯å ã export ããŠããªããã圱é¿ãªã |
ç Žå£ç倿Žã¯ãµããã¹ã®åé€ 1 ç¹ã®ã¿ãMIGRATING.md ã®ãFrom v11ãã«èšèŒããã
stripDiscordMarkdown() / stripMfm() / stripMarkdown() ã¯å
ã
ãé€å»ã㊠plain text ã«ãããæ©èœããæããªãã£ãïŒÂ§2.2ïŒãv12 ã§ã¯ãé€å»ã§ã¯ãªã倪åã»æäœã»äžç·ã»åãæ¶ãç·ãå®éã«ç»åãžæç»ããéžæè¢ã远å ããã
æ°èšãã MarkdownMode ã¯æ¹èšãéžã¶ 1 ã€ã®ãªãã·ã§ã³ã§ãsetFromMessage/setFromNote/setFromTweet/setText/setFromObject ã®ååŒã³åºããšãnew MiQ({ markdown }) ã®ããã±ãŒãžå
šäœããã©ã«ãã«çœ®ã:
type MarkdownMode = true | 'discord' | 'twitter' | 'misskey' | 'raw' | falsefalseïŒé€å»ïŒæ¢åã® stripX() ãšåãæåã»åãã¿ã€ãã³ã°ïŒã'raw'ïŒç¡å å·¥ãtrueïŒæšæº CommonMark+GFM æç»ã'discord'/'misskey'/'twitter'ïŒããããã®æ¹èšã®æç»ãããã©ã«ãã¯ååŒã³åºãã®æŽå²çããã©ã«ããå®å
šã«ä¿ã€ïŒDiscord/Tweet/æ±çšã¯ 'raw'ãNote 㯠falseïŒããããªããã€ã³ããªãéãæ¢åãŠãŒã¶ãŒã®èŠãç®ã¯äžåå€ãããªããæåãµã€ãºãå€ããç³»ïŒèŠåºããMFM ã® small/centerïŒã¯ä»åä¿ç â æ§é ã ãä¿æãããã¬ãŒã³ããã¹ããšããŠæç»ããã
é€å»çšãšæç»çšãæ¹èšããšã«å¥ã
ã®é¢æ°ãšããŠæã€ãšãæšãæ©ãããžãã¯ã 2 éåãããããã§åæ¹èšã«ã€ãåžžã« StyledRun[]ïŒ{ value, style? } ã®é
åïŒãè¿ãããŒãµãŒã 1 ã€ã ãæã¡ïŒparseMarkdown / parseDiscordMarkdown / parseMfm / parseTwitterTextãããããéå
¬é â å
¬é API ã¯æ¢åã® stripX() 3 颿°ã®ã¿æ®ã眮ãïŒãé€å»ã¯ãã®åºåããã¹ã¿ã€ã«ãæšãŠãŠæååã«æœ°ãã ãã®èãåŸåŠçïŒplainTextOf()ïŒãšããŠè¡šçŸãããstripMarkdown() çã®å
¬éã·ã°ããã£ã»æåã¯å€ããŠããªãã
discomd 㯠strip()/toHTML() ã®ä»ã«äœçœ®ããŒã¹ã® parse(): Token[] ãå
¬éããŠãããããããã®ãŸãŸã¹ã¿ã€ã«æœåºã«è»¢çšã§ããïŒDiscord æ¹èšãäžçªç°¡åã ã£ãçç±ïŒãmfm-js 㯠<b>/<i>/<s> ã¿ã°ãå
ã
bold/italic/strike ããŒããšããŠããŒã¹ãããããmarkdown-it åŽã®ãããªèªåã® HTML ã¿ã°å¯Ÿå¿ã¯äžèŠã ã£ãã
æšæº Markdown ã¢ãŒãïŒtrueïŒã§ã¯ <u>ïŒCommonMark ã«å¯Ÿå¿æ§æãç¡ãïŒã»<b>/<strong>ã»<i>/<em>ã»<s>/<del> ãè£
食ãšããŠè§£éãããmarkdown-it ã¯ç HTML ã html_inline ãšãããã©ãããªããŒã¯ã³ãšããŠããåºããªãïŒstrong_open/strong_close ã®ããã«æšãšããŠå
¥ãåã«ããªãïŒããã察ã«ãªãéå§ã»çµäºã¿ã°ãèªåã§ãã¢ãªã³ã°ããå°ããªå€æïŒpairHtmlTagsãtext/markdown.tsïŒã远å ããã
æ€èšãããäžæ¡çšã«ãããã®:
- turndownïŒHTMLâMarkdown 倿ïŒ: ä»åã®å ¥åã¯ãMarkdown æ¬æã«ãšããã©ããç HTML ã¿ã°ãæ··ãã£ããã®ãã§ãããturndown ã¯ãHTML å šäœã Markdown ã«å€æãããããã®ããŒã«ãªã®ã§åœ¢ãåããªãã
rehype-rawïŒunified/remark-rehype ç³»ïŒ: markdown å ã®ç HTML ãparse5ã§æ£åŒã« HTML5 仿§éãããŒã¹ãçŽããæããæ£ãããéžæè¢ããã ãå ¬åŒããã¥ã¡ã³ãèªèº«ããããã©ãŒãã³ã¹/ãã³ãã«ãµã€ãºçã«éãåŠçããšæèšããŠãããunified+remark-parse+remark-rehype+rehype-rawã®æäœ 4 ããã±ãŒãžãå¿ èŠã«ãªãã<u>/<b>/<i>/<s>ã® 6 ã¿ã°ã®åçŽãªééãã¢ãªã³ã°ã®ããã ãã«å°å ¥ããã«ã¯äžé£ãåãã«éãã
çµè«ãšããŠãæ°èŠäŸåã远å ããªãèªåã®å°ããªãã¢ãªã³ã°åŠçãæ¡çšããïŒÂ§2.2 ã§ displus ãåã£ããé€å»ã®ããã ãã®èãäŸåãå¢ãããããªãããšãã倿ãšåãçç±ïŒã
XïŒTwitterïŒã®æçš¿ APIã»FxTwitter ã®ãããã text ã¯ãã¬ãŒã³ãªæååã®ã¿ãè¿ãã倪å/æäœãè¡šãæ§é åãã£ãŒã«ãã¯ååšããªãïŒWeb æ€çŽ¢ã§ç¢ºèªïŒããTwitter ã®å€ªåãã¯å®éã«ã¯ Unicode ã®æ°åŠè±æ°èšå·ãããã¯ïŒU+1D400âU+1D7FFïŒãã¯ã©ã€ã¢ã³ãåŽãŸãã¯ããŒã«ãæååäœã§çœ®æããŠããã ãã§ãæ§é åãããããŒã¯ã§ã¯ãªãããã®ãã 'twitter' ã¢ãŒãã¯ç¬èªã®ããŒãµãŒïŒtext/twitterText.tsïŒãšããŠãããã®ã³ãŒããã€ã³ãç¯å²ãæ€åºããASCII ã«æ£èŠåãã€ã€ã¹ã¿ã€ã«ãä»äžãããç¯å²ã¯é£ç¶ãããªãã»ããèšç®æ°åã§å®è£
ããŠãããã«ãã¯ã¢ããããŒãã«ã¯æããªãã
MiQConversation: äŸé Œã»å®äŸãåäžã® quote (MiQ) ã«ã€ããŠã®ãã®ã ã£ããããä»åã¯å¯Ÿè±¡å€ãconversationPipeline.tsãžã®å圢ã®è¿œå å®è£ ã¯å°æ¥ã®èª²é¡ãšããŠæ®ãã- æåãµã€ãºãå€ããæ§æïŒèŠåºããMFM ã®
small/centerïŒ: ã¹ã¿ã€ã«ïŒå€ªå/æäœ/äžç·/åãæ¶ãç·ïŒãšãµã€ãºå€æŽã¯å¥çš®ã®å€æŽã§ãããåŸè ã¯ä¿çã - å
¬é API ãšããŠã®
parseX()/StyledRun:stripX()3 颿°ã¯æ®ã眮ãã§å ¬éã®ãŸãŸã ããæ°èšããparseMarkdown/parseDiscordMarkdown/parseMfm/parseTwitterTextãšStyledRunåã¯éå ¬éã®ãŸãŸãæ¢åã®segmentText()çtext/é äžã®ãã«ããŒãåæ§ã«éå ¬éã§ããããã®æ £äŸã«åãããã
2ã€ã®ç¬ç«ãã远å :
WatermarkThemeã¯ããã¹ãã®ã¿ã ã£ãããMiQ#setWatermark()ãstring | URL | Buffer | Uint8Arrayãåãåããããã«ããããŽãªã©ã®ç»åã watermark äœçœ®ã«æç»ã§ããããã«ãããMiQConversationïŒÂ§20.5 ã§ãä»åã¯ã¹ã³ãŒãå€ããšããè€æ°ã¡ãã»ãŒãžã®ãã£ãããã°æç»ïŒãå³åº§ã«åé€ãã代ããã«æ°èšMiQChainã远å ãããDiscordã»Xã»Misskey ã«ã¯ãªãã©ã€å ã»åŒçšå ãããããå æçš¿ïŒè¿ä¿¡ãã1æã®ç»åã«ããããšããèŠæã«å¯ŸããŠãMiQConversationã®å»¶é·ïŒãã£ãããã°ã®è¡æ°ãå¢ããïŒã§ã¯ãªããæ¢åã®æšªåãMiQç»åã2æãã®ãŸãŸäžäžã«ãã£ã€ãããšããå¥ã®åœ¢ã§å¿ããã
QuoteData.watermark: string ãš QuoteData.watermarkImage: AvatarSource | null ã®2ãã£ãŒã«ãã«åããäºãã«æä»ãšããïŒavatar ãšéããwatermark ã§ã¯æååã¯åžžã«ããã¹ããšããŠæ±ã â URL/Buffer/Uint8Array ã ããç»åïŒãnormalizeWatermarkInput()ïŒcore/quote.tsïŒãå
¬éã® union å
¥åããã®2ãã£ãŒã«ããžæ¯ãåããã
æç»åŽã¯ theme.watermark.size ããããã¹ãã§ã¯ãã©ã³ããµã€ãºãšããŠãç»åã§ã¯é«ããšããŠæµçšããïŒããã¹ãâç»åãå·®ãæ¿ããŠãèŠãç®ã®ã¹ã±ãŒã«æãå€ãããªãããã«ããããïŒãcolor/font/weight ã¯ç»åã«ã¯é©çšãããªãã
API ã¯ãæ¢ã«åå¥ã«çµã¿ç«ãŠã2ã€ã® MiQ ã€ã³ã¹ã¿ã³ã¹ãæž¡ãã圢 new MiQChain(top, bottom, options?) ã«ããïŒãŠãŒã¶ãŒç¢ºèªæžã¿ïŒãçç±: å MiQ ã¯æ¢ã« themeã»boldã»colorã»markdown ãªã©ãæã£ãŠããã®ã§ãMiQChain èªèº«ããããã®ãªãã·ã§ã³äœç³»ãåçºæããå¿
èŠããªããMiQChain ãæ±ãã®ã¯ãã©ã¡ãã®ååã® avatar.position ãã©ã¡ãã®èŸºã«ãããããšããã2æãçµã¿åããããšãã«ã ãæå³ãæã€é¢å¿äºã ãã«çµã£ãïŒChainOptions.flip/topFlip/bottomFlipïŒã
ããã©ã«ãã®ãã¢ãªã³ã°ã¯ äž='right'ãäž='left'ïŒREADMEãFlipping sidesãã§ãã flip/unflipïŒãflip: true ã§å転ãtopFlip/bottomFlip ã§çåŽã ãæç€ºçã«äžæžãã§ããã
å®è£
äžã®èœãšã穎: åœå top.clone().setTheme({ avatar: { position } }) ãšããŠããããMiQ#setTheme() ã¯ã€ã³ã¹ã¿ã³ã¹ã®çŸåšã®ããŒãã«ããŒãžããã®ã§ã¯ãªããdefineTheme() ã§æ¯åãŒãããçµã¿ç«ãŠçŽãïŒextends ãã¬ãã + æž¡ããå
¥åã®ã¿ããïŒããã®ããéšåãªããžã§ã¯ããæž¡ããšãåŒã³åºãå
ãèšå®ããŠãã layout: 'new' ãå«ãä»ã®ããŒãèšå®ãé»ã£ãŠ 'dark'/'side' ã®ããã©ã«ãã«å·»ãæ»ã£ãŠããŸããã°ããã£ãïŒlayout: 'new' çŠæ¢ã®ãã¹ããæžããŠå®éã«æ€åºïŒãä¿®æ£: getTheme()ïŒstructuredClone æžã¿ïŒã§å®å
šãªããŒãã®ã¹ãããã·ã§ãããåããavatar.position ã ãããã®ãªããžã§ã¯ãäžã§æžãæããŠãã setTheme() ã«äžžããšæž¡ã â defineTheme() ã¯æž¡ãããå®å
šãªããŒãã®å
šãã£ãŒã«ãïŒlayout ãå«ãïŒã§äžæžããããããå®è³ªçã«ã1ãã£ãŒã«ãã ãå€ããã³ããŒãã«ãªãã
layout: 'new' ã¯å·Šå³ã®ã¢ãã¿ãŒããã¯ã¹ãšããæŠå¿µèªäœãç¡ããããä»åã¯é察å¿ãšã ValidationError ãæããã2æã®å¹
ãäžèŽããªãå Žåããæé»ã®ãªãµã€ãº/åŒã䌞ã°ãã¯ãã ValidationError ã§æ¢ããã
src/core/MiQConversation.tsã»src/core/conversation.tsã»src/render/conversationPipeline.tsïŒããããã® .test.ts å«ãïŒãåé€ãConversationMessage/ConversationOptions/ConversationThemeName åãš index.ts ããã® export ãåé€ãCLIã»examples ã«åç
§ã¯ç¡ãã£ããMIGRATING.md ã®ãFrom v11ãã«ç Žå£ç倿ŽãšããŠèšèŒã
{ "name": "makeitaquote", "version": "9.0.0", "description": "Generate \"Make it a Quote\" images locally, or via an external API.", "keywords": [ "img", "image", "gen", "generate", "miq", "makeitaquote", "quote", "discord", "canvas", "api" ], "homepage": "https://github.com/otnc/makeitaquote#readme", "bugs": { "url": "https://github.com/otnc/makeitaquote/issues" }, "repository": { "type": "git", "url": "git+https://github.com/otnc/makeitaquote.git" }, "license": "ISC", "author": "otoneko.", "type": "module", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.mts", "exports": { ".": { "types": { "import": "./dist/index.d.mts", "require": "./dist/index.d.cts" }, "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, "./api": { "types": { "import": "./dist/api/index.d.mts", "require": "./dist/api/index.d.cts" }, "import": "./dist/api/index.mjs", "require": "./dist/api/index.cjs" }, "./package.json": "./package.json" }, "files": ["dist"], "engines": { "node": ">=22" }, "scripts": { "build": "tsdown", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", "format": "biome format --write", "format:check": "biome format", "lint": "biome lint", "lint:fix": "biome lint --write", "check": "biome check --write", "ci": "biome ci", "check:build": "node scripts/check-build.js", "prepublishOnly": "npm run build" }, "dependencies": { "@napi-rs/canvas": "^1.0.3", "@twemoji/parser": "^17.0.2", "budoux": "^0.8.4", "ky": "^2.0.2" }, "devDependencies": { "@biomejs/biome": "^2.5.6", "@types/node": "^22.20.1", "@vitest/coverage-v8": "^4.1.10", "tsdown": "^0.22.14", "typescript": "^6.0.3", "vitest": "^4.1.10" } }