├── packages ├── zenn-model │ ├── .gitignore │ ├── vitest.config.ts │ ├── README.md │ ├── tsconfig.build.json │ ├── eslint.config.mjs │ ├── LICENSE │ ├── package.json │ └── src │ │ └── types.ts ├── zenn-embed-elements │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ ├── load-stylesheet.ts │ │ │ └── load-script.ts │ │ └── classes │ │ │ └── katex.ts │ ├── README.md │ ├── tsconfig.json │ ├── LICENSE │ ├── eslint.config.mjs │ └── package.json ├── zenn-content-css │ ├── .gitignore │ ├── src │ │ ├── _utils.scss │ │ ├── _variables.scss │ │ ├── _footnotes.scss │ │ ├── _message.scss │ │ ├── _prism.scss │ │ └── _embed.scss │ ├── README.md │ ├── package.json │ └── LICENSE ├── zenn-markdown-html │ ├── .gitignore │ ├── src │ │ ├── @types │ │ │ └── prism.d.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── markdown-it.ts │ │ │ ├── md-image.ts │ │ │ ├── md-source-map.ts │ │ │ ├── md-link-attributes.ts │ │ │ ├── highlight.ts │ │ │ ├── toc.ts │ │ │ ├── md-container.ts │ │ │ └── md-br.ts │ │ ├── types.ts │ │ └── markdown-to-simple-html.ts │ ├── vitest.config.ts │ ├── .babelrc.json │ ├── tsconfig.build.json │ ├── __tests__ │ │ ├── matchers │ │ │ ├── isBlueprintUEUrl.test.ts │ │ │ ├── isFigmaUrl.test.ts │ │ │ ├── isCodesandboxUrl.test.ts │ │ │ ├── isYoutubeUrl.test.ts │ │ │ └── isDocswellUrl.test.ts │ │ ├── br.test.ts │ │ ├── custom-syntax │ │ │ ├── embed │ │ │ │ ├── jsfiddle.test.ts │ │ │ │ ├── youtube.test.ts │ │ │ │ ├── slideshare.test.ts │ │ │ │ ├── stackblitz.test.ts │ │ │ │ ├── codepen.test.ts │ │ │ │ ├── blueprintue.test.ts │ │ │ │ ├── figma.test.ts │ │ │ │ ├── codesandbox.test.ts │ │ │ │ ├── tweet.test.ts │ │ │ │ ├── gist.test.ts │ │ │ │ ├── github.test.ts │ │ │ │ ├── card.test.ts │ │ │ │ ├── mermaid.test.ts │ │ │ │ └── speakerdeck.test.ts │ │ │ └── messagebox.test.ts │ │ └── basic.test.ts │ ├── eslint.config.mjs │ ├── LICENSE │ ├── README.md │ └── package.json └── zenn-cli │ ├── .prettierignore │ ├── articles │ ├── 902-no-markdown-file.js │ ├── 901-example-no-frontmatters.md │ ├── 900-example-empty.md │ ├── 301-example-embed-github.md │ ├── 200-example-images.md │ ├── 302-example-embed-x.md │ ├── 101-example-toc-check.md │ ├── 300-example-embed-basics.md │ ├── 305-example-embed-others.md │ └── 303-example-embed-mermaid.md │ ├── books │ ├── chapter-by-config │ │ ├── nothing.md │ │ ├── test.md │ │ ├── conclusion.md │ │ ├── 1.one.md │ │ ├── 1234.md │ │ ├── about.md │ │ └── config.yaml │ ├── chapter-by-filename │ │ ├── 9.md │ │ ├── 3.c++.md │ │ ├── 3.cpp.md │ │ ├── conclusion.md │ │ ├── 1.about.md │ │ ├── 999.conclusion.md │ │ ├── 2.hello-world.md │ │ ├── cover.png │ │ ├── config.yaml │ │ └── 4.exmple-images.md │ ├── README.md │ ├── empty-book │ │ ├── cover.webp │ │ └── config.yaml │ └── .ignore-book │ │ └── config.yaml │ ├── src │ ├── server │ │ ├── __tests__ │ │ │ ├── fixtures │ │ │ │ ├── empty │ │ │ │ │ └── .keep │ │ │ │ ├── books │ │ │ │ │ ├── my-first-book │ │ │ │ │ │ ├── .keep │ │ │ │ │ │ ├── example1.md │ │ │ │ │ │ ├── example3.md │ │ │ │ │ │ ├── example2.md │ │ │ │ │ │ ├── cover.jpg │ │ │ │ │ │ └── config.yaml │ │ │ │ │ └── my-second-book │ │ │ │ │ │ ├── .keep │ │ │ │ │ │ ├── 1.hi.md │ │ │ │ │ │ ├── 2.hey.md │ │ │ │ │ │ ├── invalid-slug.md │ │ │ │ │ │ └── config.yml │ │ │ │ ├── empty-directories │ │ │ │ │ ├── books │ │ │ │ │ │ └── .keep │ │ │ │ │ └── articles │ │ │ │ │ │ └── .keep │ │ │ │ ├── markdown-body-only.md │ │ │ │ ├── images │ │ │ │ │ ├── test.jpg │ │ │ │ │ └── test-1036bytes.jpg │ │ │ │ └── articles │ │ │ │ │ ├── my-first-post.md │ │ │ │ │ └── my-second-post.md │ │ │ ├── commands │ │ │ │ ├── version.test.ts │ │ │ │ ├── help.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ └── init.test.ts │ │ │ └── lib │ │ │ │ ├── log.test.ts │ │ │ │ └── notify-update.test.ts │ │ ├── types.ts │ │ ├── commands │ │ │ ├── help.ts │ │ │ ├── version.ts │ │ │ ├── preview.ts │ │ │ ├── index.ts │ │ │ ├── list-books.ts │ │ │ ├── list-articles.ts │ │ │ └── init.ts │ │ ├── zenn.ts │ │ ├── api │ │ │ ├── local-info.ts │ │ │ ├── cli-version.ts │ │ │ ├── articles.ts │ │ │ ├── manual.ts │ │ │ └── books.ts │ │ ├── lib │ │ │ ├── log.ts │ │ │ ├── history-fallback.ts │ │ │ ├── notify-update.ts │ │ │ └── server.ts │ │ └── app.ts │ ├── client │ │ ├── public │ │ │ ├── favicon.png │ │ │ ├── static-images │ │ │ │ ├── book-cover.png │ │ │ │ ├── folder-close.svg │ │ │ │ ├── folder-open.svg │ │ │ │ └── copy-icon.svg │ │ │ └── logo.svg │ │ ├── types.ts │ │ ├── hooks │ │ │ ├── useTitle.ts │ │ │ ├── useFetch.ts │ │ │ ├── useLocalFileChangedEffect.tsx │ │ │ └── usePersistedState.ts │ │ ├── lib │ │ │ └── helper.ts │ │ ├── index.tsx │ │ ├── components │ │ │ ├── ContentContainer.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Loading.tsx │ │ │ ├── PropertyRow.tsx │ │ │ ├── BodyContent.tsx │ │ │ ├── App.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── ErrorMessage.tsx │ │ │ ├── PrintDetailsOpener.tsx │ │ │ ├── TopicList.tsx │ │ │ ├── guide │ │ │ │ └── index.tsx │ │ │ ├── books │ │ │ │ └── show │ │ │ │ │ └── BookChapterList.tsx │ │ │ ├── sidebar │ │ │ │ ├── ListItemInner.tsx │ │ │ │ └── Directory.tsx │ │ │ └── articles │ │ │ │ └── show.tsx │ │ ├── index.html │ │ └── __tests__ │ │ │ └── lib │ │ │ └── helper.test.ts │ └── common │ │ ├── types.ts │ │ ├── helper.ts │ │ └── __tests__ │ │ └── helper.test.ts │ ├── .gitignore │ ├── .env.example │ ├── images │ └── example-images │ │ ├── zenn-editor.png │ │ └── zenn-editor.webp │ ├── vitest.server.config.ts │ ├── nodemon.json │ ├── vitest.client.config.ts │ ├── tsconfig.server.json │ ├── vitest.e2e.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.client.json │ ├── eslint.config.mjs │ ├── LICENSE │ └── vite.config.ts ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── lerna.json ├── .secretlintrc.json ├── .devcontainer ├── init.sh ├── devcontainer.json └── Dockerfile ├── .prettierrc.js ├── rulesync.jsonc ├── pnpm-workspace.yaml ├── .claude └── settings.json ├── turbo.json ├── .rulesync ├── rules │ └── pull-request.md ├── commands │ └── release-pr.md └── mcp.json ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── release-drafter.yml ├── workflows │ ├── lint.yml │ ├── test.yml │ ├── validate-pr.yml │ └── e2e.yml └── dependabot.yml ├── LICENSE ├── .gitignore ├── docs └── pull_request_policy.md ├── package.json └── README.md /packages/zenn-model/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | *.md 3 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /packages/zenn-content-css/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /packages/zenn-cli/.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | *.md 3 | -------------------------------------------------------------------------------- /packages/zenn-cli/articles/902-no-markdown-file.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/nothing.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/test.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/conclusion.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/empty/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/articles/901-example-no-frontmatters.md: -------------------------------------------------------------------------------- 1 | body 2 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-second-book/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/empty-directories/books/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/9.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: slugなし 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/empty-directories/articles/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/1.one.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'slugが不正' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/3.c++.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'C++' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/3.cpp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'C++' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/conclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/1.about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/999.conclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/@types/prism.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'prismjs/components/'; 2 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/markdown-body-only.md: -------------------------------------------------------------------------------- 1 | # Hello 2 | 3 | Hola! 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/1234.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'slugが数字のみ' 3 | --- 4 | 5 | 1234 6 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'about' 3 | free: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/types.ts: -------------------------------------------------------------------------------- 1 | export type CliExecFn = (argv?: string[]) => void | Promise; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | yarn-error.log 4 | /dist 5 | /temp 6 | .temp 7 | .env -------------------------------------------------------------------------------- /packages/zenn-cli/books/README.md: -------------------------------------------------------------------------------- 1 | - 開発用 example コンテンツ 2 | - プロジェクトルートの`./books`からマークダウンや設定ファイルを読み込んでプレビュー 3 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/2.hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Hello' 3 | --- 4 | 5 | Hello 6 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/example1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title1' 3 | --- 4 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { parseToc } from './toc'; 2 | 3 | export { parseToc }; 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.2.11", 6 | "npmClient": "pnpm" 7 | } 8 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-second-book/1.hi.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title1' 3 | free: true 4 | --- 5 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/example3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title3' 3 | free: false 4 | --- 5 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-second-book/2.hey.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title2' 3 | free: false 4 | --- 5 | -------------------------------------------------------------------------------- /.secretlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "id": "@secretlint/secretlint-rule-preset-recommend" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/zenn-cli/.env.example: -------------------------------------------------------------------------------- 1 | # 埋め込みサーバーのオリジン(viteにも対応するために`VITE_`をつけてます) 2 | VITE_EMBED_SERVER_ORIGIN="https://embed.zenn.studio" 3 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-second-book/invalid-slug.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title3' 3 | free: false 4 | --- 5 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/utils/markdown-it.ts: -------------------------------------------------------------------------------- 1 | import markdownit from 'markdown-it'; 2 | 3 | export const md = markdownit(); 4 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/empty-book/cover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/books/empty-book/cover.webp -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/example2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'title2' 3 | free: true 4 | --- 5 | 6 | Hello! 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/src/client/public/favicon.png -------------------------------------------------------------------------------- /packages/zenn-cli/src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { itemSortTypes } from './helper'; 2 | 3 | export type ItemSortType = (typeof itemSortTypes)[number]; 4 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/src/index.ts: -------------------------------------------------------------------------------- 1 | import { EmbedKatex } from './classes/katex'; 2 | 3 | customElements.define('embed-katex', EmbedKatex); 4 | -------------------------------------------------------------------------------- /.devcontainer/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mise trust 4 | mise use node@24 pnpm@10 python@3 uv@latest 5 | mise install 6 | pnpm i 7 | gh auth setup-git 8 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/.ignore-book/config.yaml: -------------------------------------------------------------------------------- 1 | title: '`.`から始まる本は無視されます' 2 | summary: '' 3 | published: false 4 | topics: 5 | - ignore 6 | price: 200 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/books/chapter-by-filename/cover.png -------------------------------------------------------------------------------- /packages/zenn-cli/images/example-images/zenn-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/images/example-images/zenn-editor.png -------------------------------------------------------------------------------- /packages/zenn-cli/images/example-images/zenn-editor.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/images/example-images/zenn-editor.webp -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | }; -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/config.yaml: -------------------------------------------------------------------------------- 1 | title: ファイル名でチャプターの並び順を指定する本 2 | summary: '' 3 | topics: [] 4 | published: true 5 | price: 0 # 有料の場合200〜5000 6 | 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/empty-book/config.yaml: -------------------------------------------------------------------------------- 1 | title: 空の本 2 | summary: ここに好きな文章を書きます。本の内容などです 3 | published: false 4 | topics: 5 | - react 6 | - テスト 7 | price: 200 8 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/src/server/__tests__/fixtures/images/test.jpg -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/public/static-images/book-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/src/client/public/static-images/book-cover.png -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/types.ts: -------------------------------------------------------------------------------- 1 | export type ValidationError = { 2 | message: string; 3 | isCritical: boolean; 4 | detailUrl?: string; 5 | detailUrlText?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-second-book/config.yml: -------------------------------------------------------------------------------- 1 | title: 'Second' 2 | summary: 'Hello, again!' 3 | topics: ['zenn', 'cli'] 4 | published: false 5 | price: 500 6 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/images/test-1036bytes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/src/server/__tests__/fixtures/images/test-1036bytes.jpg -------------------------------------------------------------------------------- /packages/zenn-cli/articles/900-example-empty.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Empty Articleのテスト' 3 | type: 'idea' # or "idea" 4 | topics: 5 | - React 6 | - Rust 7 | emoji: 👩‍💻 8 | published: false 9 | --- 10 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenn-dev/zenn-editor/HEAD/packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/cover.jpg -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/books/my-first-book/config.yaml: -------------------------------------------------------------------------------- 1 | title: 'First' 2 | summary: 'Hello!' 3 | topics: [] 4 | published: true 5 | price: 0 6 | chapters: 7 | - example2 8 | - example1 9 | -------------------------------------------------------------------------------- /packages/zenn-content-css/src/_utils.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use './variables' as variables; 3 | 4 | @mixin mq($size) { 5 | @media #{map.get(variables.$breakpoints, $size)} { 6 | @content; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/articles/my-first-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'First' 3 | emoji: '📝' 4 | type: 'tech' # tech: 技術記事 / idea: アイデア 5 | topics: [] 6 | published: true 7 | --- 8 | 9 | Hello! 10 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/fixtures/articles/my-second-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Second' 3 | emoji: '💯' 4 | type: 'idea' 5 | topics: ['zenn', 'cli'] 6 | published: false 7 | --- 8 | 9 | Hello, again! 10 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { CliExecFn } from '../types'; 2 | import { commandListText } from '../lib/messages'; 3 | 4 | export const exec: CliExecFn = () => { 5 | console.log(commandListText); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/hooks/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function useTitle(title?: string) { 4 | useEffect(() => { 5 | if (!title) return; 6 | document.title = title; 7 | }, [title]); 8 | } 9 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/lib/helper.ts: -------------------------------------------------------------------------------- 1 | export function encodeUrlPeriod(url: string) { 2 | return escape(url.replace(/\./g, '%2E')); 3 | } 4 | export function decodeUrlPeriod(url: string) { 5 | return unescape(url).replace(/%2E/g, '.'); 6 | } 7 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/public/static-images/folder-close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { App } from './components/App'; 3 | 4 | const root = ReactDOM.createRoot( 5 | document.getElementById('root') as HTMLElement 6 | ); 7 | root.render(); 8 | -------------------------------------------------------------------------------- /packages/zenn-model/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['./__tests__/**/*.test.ts'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['./__tests__/**/*.test.ts'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/zenn-cli/vitest.server.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['./src/server/__tests__/**/*.test.ts'], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EmbedGeneratorList } from './embed'; 2 | 3 | /** 4 | * Markdown 変換時のオプション型 5 | */ 6 | export type MarkdownOptions = { 7 | embedOrigin?: string; 8 | customEmbed?: Partial; 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Launch Program", 8 | "skipFiles": ["/**"], 9 | "port": 9229 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": "node 16.0", 3 | "presets": ["@babel/preset-env", "@babel/preset-typescript"], 4 | "plugins": [ 5 | [ 6 | "prismjs", 7 | { 8 | "languages": "all" 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/zenn-model/README.md: -------------------------------------------------------------------------------- 1 | # zenn-model 2 | 3 | 記事や本のファイル名やメタ情報(frontmatter)などのユーティリティ型や関数などをまとめたパッケージです。 4 | 5 | ## 使い方 6 | 7 | zenn-cli の実装を参照してください。 8 | 9 | ## 開発者向けドキュメント 10 | 11 | https://zenn-dev.github.io/zenn-docs-for-developers/guides/zenn-editor/zenn-model 12 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-config/config.yaml: -------------------------------------------------------------------------------- 1 | title: 設定ファイルでチャプターの並び順を指定する本 2 | summary: ここに好きな文章を書きます。本の内容などです 3 | published: true 4 | topics: 5 | - react 6 | - テスト 7 | price: 200 8 | chapters: 9 | - about 10 | - test 11 | - conclusion 12 | - '1234' 13 | - 1.one 14 | -------------------------------------------------------------------------------- /packages/zenn-cli/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | ".git", 4 | "node_modules/**/node_modules", 5 | "lib/**/*", 6 | "**/__tests__/**", 7 | "*.test.ts" 8 | ], 9 | "execMap": { 10 | "ts": "node -r esbuild-register" 11 | }, 12 | "ext": "ts,js,json" 13 | } 14 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/components/ContentContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ContentContainer = styled.div` 4 | max-width: 760px; 5 | margin: 0 auto; 6 | padding: 0 1.8rem; 7 | @media (max-width: 468px) { 8 | padding: 0 1rem; 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /packages/zenn-cli/vitest.client.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: [ 8 | './src/client/__tests__/**/*.test.ts', 9 | './src/common/__tests__/**/*.test.ts', 10 | ], 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/public/static-images/folder-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "es5", 6 | "module": "commonjs", 7 | "rootDir": ".", 8 | "outDir": "./dist", 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["src/client", "node_modules", "**/__tests__"] 12 | } 13 | -------------------------------------------------------------------------------- /rulesync.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "targets": ["copilot", "cursor", "claudecode", "codexcli"], 3 | "features": ["rules", "ignore", "mcp", "commands", "subagents"], 4 | "baseDirs": ["."], 5 | "delete": true, 6 | "verbose": false, 7 | "global": false, 8 | "simulateCommands": false, 9 | "simulateSubagents": false, 10 | "modularMcp": true, 11 | } 12 | -------------------------------------------------------------------------------- /packages/zenn-cli/vitest.e2e.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['./src/e2e/e2e.ts'], 8 | pool: 'forks', 9 | poolOptions: { 10 | forks: { 11 | singleFork: true, 12 | }, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/zenn-cli/README.md: -------------------------------------------------------------------------------- 1 | # zenn-cli 2 | 3 | ローカルで Zenn の投稿コンテンツを管理/プレビューするための CLI です。 4 | 5 | ## ユーザー向けドキュメント 6 | 7 | - [CLI のインストール](https://zenn.dev/zenn/articles/install-zenn-cli) 8 | - [CLI の使い方](https://zenn.dev/zenn/articles/zenn-cli-guide) 9 | 10 | ## 開発者向けドキュメント 11 | 12 | https://zenn-dev.github.io/zenn-docs-for-developers/guides/zenn-editor/zenn-cli 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | 4 | minimumReleaseAge: 30240 5 | 6 | # 2025/10/28 に `pnpm audit --fix` を実行した結果 7 | overrides: 8 | '@babel/runtime@<7.26.10': '>=7.26.10' 9 | cross-spawn@<6.0.6: '>=6.0.6' 10 | form-data@>=4.0.0 <4.0.4: '>=4.0.4' 11 | nanoid@<3.3.8: '>=3.3.8' 12 | postcss@<8.4.31: '>=8.4.31' 13 | semver@<5.7.2: '>=5.7.2' 14 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/commands/version.ts: -------------------------------------------------------------------------------- 1 | import { CliExecFn } from '../types'; 2 | import { getCurrentCliVersion } from '../lib/helper'; 3 | import * as Log from '../lib/log'; 4 | 5 | export const exec: CliExecFn = () => { 6 | const version = getCurrentCliVersion(); 7 | if (!version) { 8 | Log.error('zenn-cliのバージョンを取得できませんでした'); 9 | return; 10 | } 11 | console.log(version); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/zenn-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": false, 5 | "skipLibCheck": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "jsx": "preserve", 11 | "baseUrl": "./src", 12 | "lib": ["ESNext", "dom"] 13 | }, 14 | "exclude": ["**/__tests__"], 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/zenn.ts: -------------------------------------------------------------------------------- 1 | // This file is called from npm bin script. See package.json for details 2 | 3 | import arg from 'arg'; 4 | import { exec } from './commands'; 5 | 6 | const args = arg( 7 | {}, 8 | { 9 | permissive: true, 10 | } 11 | ); 12 | const execCommandName = args._[0] || 'preview'; 13 | const execCommandArgs = args._.slice(1); 14 | 15 | // call command 16 | exec(execCommandName, execCommandArgs, { canNotifyUpdate: true }); 17 | -------------------------------------------------------------------------------- /packages/zenn-content-css/src/_variables.scss: -------------------------------------------------------------------------------- 1 | /* border-radius */ 2 | $rounded-xs: 4px; 3 | $rounded-sm: 7px; 4 | $rounded-md: 10px; 5 | $rounded-lg: 14px; 6 | $rounded-xl: 20px; 7 | $rounded-full: 99rem; 8 | $rounded-publication: 25%; 9 | 10 | $xs: 374px; 11 | $sm: 576px; 12 | $md: 768px; 13 | 14 | $breakpoints: ( 15 | 'xs': 'screen and (max-width: #{$xs})', 16 | 'sm': 'screen and (max-width: #{$sm})', 17 | 'md': 'screen and (max-width: #{$md})', 18 | ); 19 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "BASH_DEFAULT_TIMEOUT_MS": "300000", 4 | "BASH_MAX_TIMEOUT_MS": "1200000" 5 | }, 6 | "sandbox": { 7 | "enabled": true, 8 | "excludedCommands": ["git"], 9 | "autoAllowBashIfSandboxed": true, 10 | "network": { 11 | "allowLocalBinding": true 12 | } 13 | }, 14 | "statusLine": { 15 | "type": "command", 16 | "command": "pnpm dlx ccusage statusline", 17 | "padding": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/zenn-cli/tsconfig.client.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "types": ["vite/client"], 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "module": "ESNext", 10 | "isolatedModules": false, 11 | "noEmit": true, 12 | "jsx": "react-jsx" 13 | }, 14 | "exclude": ["src/server", "node_modules", "**/__tests__"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/api/local-info.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import fs from 'fs-extra'; 3 | import { getWorkingPath } from '../lib/helper'; 4 | 5 | export async function getLocalInfo( 6 | _req: Express.Request, 7 | res: Express.Response 8 | ) { 9 | const articleDirpath = getWorkingPath('articles'); 10 | try { 11 | fs.readdirSync(articleDirpath); 12 | res.json({ hasInit: true }); 13 | } catch (_e) { 14 | res.json({ hasInit: false }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "clean": { 5 | "cache": false 6 | }, 7 | "test": { 8 | "outputs": [] 9 | }, 10 | "build": { 11 | "dependsOn": ["^build"], 12 | "outputs": ["dist/**", "lib/**"] 13 | }, 14 | "lint": { 15 | "outputs": [] 16 | }, 17 | "lint-strict": { 18 | "outputs": [] 19 | }, 20 | "fix": { 21 | "outputs": ["src/**"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/README.md: -------------------------------------------------------------------------------- 1 | # zenn-embed-elements 2 | 3 | zenn-embed-elements は、 markdown のZenn独自の埋め込み要素をHTMLに変換するためのパッケージです。現在はKaTeXによる数式のレンダリングのためにのみ利用しています。 4 | 5 | ## 使い方 6 | 7 | Reactの場合、以下のような形でモジュールを読み込みます。 8 | 9 | ```tsx 10 | export default function App(...) { 11 | useEffect(()=> { 12 | import("zenn-embed-elements") 13 | },[]) 14 | } 15 | ``` 16 | 17 | ## 開発者向けドキュメント 18 | 19 | https://zenn-dev.github.io/zenn-docs-for-developers/guides/zenn-editor/zenn-embed-elements 20 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/commands/version.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, test, expect, beforeEach } from 'vitest'; 2 | import { exec } from '../../commands/version'; 3 | 4 | describe('version コマンドのテスト', () => { 5 | console.log = vi.fn(); 6 | beforeEach(() => { 7 | console.log = vi.fn(); 8 | }); 9 | 10 | test('バージョン情報をコンソールに出力する', () => { 11 | exec([]); 12 | expect(console.log).toHaveBeenCalledWith( 13 | expect.stringMatching(/^0\.[0-9.]+/) 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/lib/log.ts: -------------------------------------------------------------------------------- 1 | import colors from 'colors/safe'; 2 | 3 | export function error(message: string) { 4 | console.error(colors.red('error:'), message); 5 | } 6 | 7 | export function success(message: string) { 8 | console.log(colors.green('success:'), message); 9 | } 10 | 11 | export function created(name: string) { 12 | console.log('created:', colors.green(name)); 13 | } 14 | 15 | export function warn(message: string) { 16 | console.warn(colors.yellow('warn:'), message); 17 | } 18 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // important for web components 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "exclude": ["node_modules", "lib"], 15 | "include": ["**/*.ts", "**/*.tsx"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/__tests__/commands/help.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, test, expect, beforeEach } from 'vitest'; 2 | import { exec } from '../../commands/help'; 3 | import { commandListText } from '../../lib/messages'; 4 | 5 | describe('helpコマンドのテスト', () => { 6 | beforeEach(() => { 7 | console.log = vi.fn(); 8 | }); 9 | 10 | test('ヘルプメッセージを表示する', () => { 11 | exec([]); 12 | expect(console.log).toHaveBeenCalledWith( 13 | expect.stringContaining(commandListText) 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/zenn-content-css/README.md: -------------------------------------------------------------------------------- 1 | # zenn-content-css 2 | 3 | zenn-markdown-html で markdown から変換された HTML に適用するためのCSSです。 4 | 5 | ## 使い方 6 | 7 | Webpackと併用する場合は、CSSローダーが必要になる場合があります。 8 | 9 | ```js 10 | import 'zenn-content-css'; 11 | ``` 12 | 13 | スタイルを適用したい要素に `class=znc` を指定します。 14 | 15 | ```html 16 |
17 | 18 |
19 | ``` 20 | 21 | zncの外側の要素にはスタイルが適用されないことに注意してください。 22 | 23 | ## 開発者向けドキュメント 24 | 25 | https://zenn-dev.github.io/zenn-docs-for-developers/guides/zenn-editor/zenn-content-css 26 | -------------------------------------------------------------------------------- /packages/zenn-model/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" /* Redirect output structure to the directory. */, 5 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 6 | "noEmit": false /* Do not emit outputs. */, 7 | "emitDeclarationOnly": true, 8 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/utils/md-image.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import type { RenderRule } from 'markdown-it/lib/renderer.mjs'; 3 | 4 | export const mdImage = (md: MarkdownIt): void => { 5 | const originalImageRenderRule = md.renderer.rules['image'] as RenderRule; 6 | 7 | md.renderer.rules.image = (tokens, idx, options, env, slf) => { 8 | const token = tokens[idx]; 9 | 10 | token.attrJoin('class', 'md-img'); 11 | token.attrSet('loading', 'lazy'); 12 | 13 | return originalImageRenderRule(tokens, idx, options, env, slf); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" /* Redirect output structure to the directory. */, 5 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 6 | "noEmit": false /* Do not emit outputs. */, 7 | "emitDeclarationOnly": true, 8 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/zenn-content-css/src/_footnotes.scss: -------------------------------------------------------------------------------- 1 | @mixin styles { 2 | .footnotes { 3 | margin-top: 3rem; 4 | color: var(--c-text-subtle); 5 | font-size: 0.9em; 6 | li::marker { 7 | color: var(--c-text-subtle); 8 | } 9 | } 10 | .footnotes-title { 11 | padding-bottom: 3px; 12 | border-bottom: solid 1px var(--c-border-emphasis); 13 | font-weight: 700; 14 | font-size: 15px; 15 | } 16 | .footnotes-list { 17 | margin: 13px 0 0; 18 | } 19 | // フォーカスされている脚注の背景色を変える 20 | .footnote-item:target { 21 | background: var(--c-bg-dim); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.rulesync/rules/pull-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | root: false 3 | targets: ['*'] 4 | description: 'Pull Requestの作成ルール' 5 | globs: ['**/*'] 6 | --- 7 | 8 | # Pull Requestの作成ルール 9 | 10 | - 原則としてDraftで作成すること 11 | - TitleとDescriptionは日本語で作成すること 12 | - Descriptionの内容は以下にすること 13 | - @.github/PULL_REQUEST_TEMPLATE.md のテンプレートに従って作成すること 14 | - 「プルリクエストに含む内容の簡潔な記述」には以下を記載すること 15 | - 仕様の変更 16 | - コードの変更 17 | - その他・備考 18 | - ユーザーをPRのassigneeとして設定すること(ユーザーのGitHubアカウント名は `gh api user --jq .login` で取得できます) 19 | - labelを付与すること 20 | - @.github/workflows/validate-pr.yml で示されているいずれかのlabel 21 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/public/static-images/copy-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Sidebar } from './Sidebar'; 3 | 4 | type Props = { 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const Layout: React.FC = ({ children }) => { 9 | return ( 10 | 11 | 14 |
{children}
15 |
16 | ); 17 | }; 18 | 19 | const StyledLayout = styled.div` 20 | display: flex; 21 | .layout__main { 22 | flex: 1; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/hooks/useFetch.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { SWRConfiguration } from 'swr'; 2 | 3 | async function fetcher(url: string): Promise { 4 | const res = await fetch(url); 5 | if (!res.ok) { 6 | const error = new Error('An error occurred while fetching the data.'); 7 | const data = await res.json(); 8 | error.message = data.message; 9 | throw error; 10 | } 11 | return res.json(); 12 | } 13 | 14 | export function useFetch(apiPath: string, configuration?: SWRConfiguration) { 15 | const result = useSWR( 16 | apiPath, 17 | fetcher, 18 | configuration 19 | ); 20 | return result; 21 | } 22 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/src/utils/load-stylesheet.ts: -------------------------------------------------------------------------------- 1 | type LoadStylesheetProps = { 2 | href: string; 3 | id: string; 4 | }; 5 | 6 | export function loadStylesheet({ href, id }: LoadStylesheetProps) { 7 | return new Promise((resolve, reject) => { 8 | if (document.getElementById(id)) return resolve(); 9 | 10 | const linkElem = document.createElement('link'); 11 | linkElem.setAttribute('href', href); 12 | linkElem.setAttribute('rel', 'stylesheet'); 13 | linkElem.setAttribute('id', id); 14 | document.head.appendChild(linkElem); 15 | linkElem.onload = () => { 16 | resolve(); 17 | }; 18 | linkElem.onerror = (e) => reject(e); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## :bookmark_tabs: Summary 2 | 3 | プルリクエストに含む内容の簡潔な記述 4 | 5 | Resolves # 6 | 7 | ## :clipboard: Tasks 8 | 9 | プルリクエストを作成いただく際、お手数ですが以下の内容についてご確認をお願いします。 10 | 11 | - [ ] :book: [Contribution Guide](https://zenn-dev.github.io/zenn-docs-for-developers/contribution) を読んだ 12 | - [ ] :woman_technologist: `canary` ブランチに対するプルリクエストである 13 | - [ ] zenn-cli で実行して正しく動作しているか確認する 14 | - [ ] 不要なコードが含まれていないか( コメントやログの消し忘れに注意 ) 15 | - [ ] XSS になるようなコードが含まれていないか 16 | - [ ] モバイル端末での表示が考慮されているか 17 | - [ ] Pull Request の内容は妥当か( 膨らみすぎてないか ) 18 | 19 | より詳しい内容は [Pull Request Policy](https://github.com/zenn-dev/zenn-editor/tree/canary/docs/pull_request_policy.md) を参照してください。 20 | -------------------------------------------------------------------------------- /packages/zenn-cli/books/chapter-by-filename/4.exmple-images.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'exmple-images' 3 | --- 4 | 5 | ## 正しい指定 6 | 7 | `![](/images/example-images/zenn-editor.png)` 8 | 9 | ![](/images/example-images/zenn-editor.png) 10 | 11 | ## 誤った指定(相対パス) 12 | 13 | `![](../../images/example-images/zenn-editor.png)` 14 | 15 | ![](../../images/example-images/zenn-editor.png) 16 | 17 | ## 対応していない拡張子 18 | 19 | `![](/images/example-images/zenn-editor.svg)` 20 | 21 | ![](/images/example-images/zenn-editor.svg) 22 | 23 | ## URL を指定 24 | 25 | `![](http://placehold.jp/54/e3f2ff/000000/720x480.png?text=zenn-editor.png)` 26 | 27 | ![](http://placehold.jp/54/e3f2ff/000000/720x480.png?text=zenn-editor.png) 28 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/__tests__/matchers/isBlueprintUEUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { isBlueprintUEUrl } from '../../src/utils/url-matcher'; 3 | 4 | describe('isBlueprintUEUrlのテスト', () => { 5 | describe('Trueを返す場合', () => { 6 | test('blueprintUEの埋め込みURL', () => { 7 | const url = 'https://blueprintue.com/render/xmdvzpam/'; 8 | expect(isBlueprintUEUrl(url)).toBe(true); 9 | }); 10 | }); 11 | 12 | describe('Falseを返す場合', () => { 13 | test('XSSを含んでいる', () => { 14 | const url = `https://blueprintue.com/render/xmdvzpam/">/`; 15 | expect(isBlueprintUEUrl(url)).toBe(false); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # release-drafter configuration 2 | # ref: https://github.com/release-drafter/release-drafter#configuration 3 | name-template: 'v$RESOLVED_VERSION 🌈' 4 | tag-template: 'v$RESOLVED_VERSION' 5 | exclude-labels: 6 | - 'release' 7 | - 'auto' 8 | categories: 9 | - title: '🚀 Features' 10 | labels: 11 | - 'feature' 12 | - title: '✨ Enhancements' 13 | labels: 14 | - 'enhancement' 15 | - title: '🐛 Bug Fixes' 16 | labels: 17 | - 'bug' 18 | - title: '🧰 Maintenance' 19 | label: 'chore' 20 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 21 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 22 | template: $CHANGES 23 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/api/cli-version.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import { getCurrentCliVersion, getPublishedCliVersion } from '../lib/helper'; 3 | 4 | export async function getCliVersion( 5 | _req: Express.Request, 6 | res: Express.Response 7 | ) { 8 | try { 9 | const latest = await getPublishedCliVersion(); 10 | if (!latest) throw "Couldn't get latest version"; 11 | const current = getCurrentCliVersion(); 12 | if (!current) throw "Couldn't get current version"; 13 | const updateAvailable = latest !== current; 14 | res.json({ current, latest, updateAvailable }); 15 | } catch (_err) { 16 | res 17 | .status(500) 18 | .json({ message: '最新のzenn-cliのバージョンを取得できませんでした' }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.rulesync/commands/release-pr.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'リリース用のプルリクエストを作成します' 3 | targets: 4 | - '*' 5 | --- 6 | 7 | 1. リポジトリの最新のリリースバージョンを取得します(Pre-release ではなく Latest release を取得)。patchバージョンを+1して今回リリースするバージョンとし、new_versionと呼称します。 8 | 2. 前回のリリースPR(titleが `release {previous_version}` になっている)より後にマージされたPRのURL(`https://github.com/zenn-dev/zenn-editor/pull/{pr_number}`)をすべて取得します。 9 | 3. 以下の内容でPRを作成します。参考: https://github.com/zenn-dev/zenn-editor/pull/561 10 | - canaryブランチ to mainブランチ 11 | - title: `release {new_version}` 12 | - description: 13 | - 以下の内容のみ。 @.github/PULL_REQUEST_TEMPLATE.md の内容は無視してください。 14 | ```md 15 | changes: 16 | - {pr_url1} 17 | - {pr_url2} 18 | - ... 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/__tests__/matchers/isFigmaUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { isFigmaUrl } from '../../src/utils/url-matcher'; 3 | 4 | describe('isFigmaUrlのテスト', () => { 5 | describe('Trueを返す場合', () => { 6 | test('Figmaの埋め込みURL', () => { 7 | const url = 8 | 'https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931/Sample-File'; 9 | 10 | expect(isFigmaUrl(url)).toBe(true); 11 | }); 12 | }); 13 | 14 | describe('Falseを返す場合', () => { 15 | test('XSSを含んでいる', () => { 16 | const url = `https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931/Sample-File">/`; 17 | expect(isFigmaUrl(url)).toBe(false); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint zenn-editor 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - canary 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v5 16 | 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | version: 10.24.0 20 | 21 | - name: Install safe-chain 22 | run: curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci 23 | env: 24 | SAFE_CHAIN_VERSION: "1.2.2" 25 | 26 | - name: パッケージをインストール 27 | run: pnpm install 28 | 29 | - name: Lint 30 | run: pnpm lint-strict 31 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/api/articles.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | 3 | import { getLocalArticle, getLocalArticleMetaList } from '../lib/articles'; 4 | import { getValidSortTypes } from '../../common/helper'; 5 | 6 | export function getArticle(req: Express.Request, res: Express.Response) { 7 | const article = getLocalArticle(req.params.slug); 8 | if (!article) { 9 | res 10 | .status(404) 11 | .json({ message: '記事のマークダウンを取得できませんでした' }); 12 | return; 13 | } 14 | res.json({ article }); 15 | } 16 | 17 | export function getArticles(req: Express.Request, res: Express.Response) { 18 | const sort = getValidSortTypes(req.query.sort); 19 | const articles = getLocalArticleMetaList(sort); 20 | res.json({ articles }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/server/api/manual.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import fetch from 'node-fetch'; 3 | 4 | type ArticleResponse = { article: { title: string; body_html: string } }; 5 | 6 | export async function getManual(req: Express.Request, res: Express.Response) { 7 | const slug = req.params.slug; 8 | try { 9 | const response = await fetch(`https://zenn.dev/api/articles/${slug}`); 10 | const { article } = (await response.json()) as ArticleResponse; 11 | const { body_html, title } = article; 12 | res.setHeader('Cache-Control', 'public, max-age=7200'); // cache on browser 13 | res.json({ bodyHtml: body_html, title }); 14 | } catch (err) { 15 | console.log(err); 16 | res.status(500).json({ message: 'エラー' }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/__tests__/matchers/isCodesandboxUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { isCodesandboxUrl } from '../../src/utils/url-matcher'; 3 | 4 | describe('isCodesandboxUrlのテスト', () => { 5 | describe('Trueを返す場合', () => { 6 | test('Codesandboxの埋め込みURL', () => { 7 | const url = 'https://codesandbox.io/embed/new?view=Editor+%2B+Preview'; 8 | 9 | expect(isCodesandboxUrl(url)).toBe(true); 10 | }); 11 | }); 12 | 13 | describe('Falseを返す場合', () => { 14 | test('XSSを含んでいる', () => { 15 | const url = `https://codesandbox.io/embed/new?view=Editor+%2B+Preview">/`; 16 | expect(isCodesandboxUrl(url)).toBe(false); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Zenn CLI 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/utils/md-source-map.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | 3 | /** 4 | * Adds begin line index to the output via the 'data-line' data attribute. 5 | * 6 | * Ref: https://github.com/microsoft/vscode/blob/84f63bf4e54c60e40865c8c4d8002893a337fe61/extensions/markdown-language-features/src/markdownEngine.ts#L17-L40 7 | */ 8 | export function mdSourceMap(md: MarkdownIt): void { 9 | // Set the attribute on every possible token. 10 | md.core.ruler.push('source_map_data_attribute', (state): void => { 11 | for (const token of state.tokens) { 12 | if (token.map && token.type !== 'inline') { 13 | token.attrSet('data-line', String(token.map[0])); 14 | token.attrJoin('class', 'code-line'); 15 | } 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/__tests__/lib/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { encodeUrlPeriod, decodeUrlPeriod } from '../../lib/helper'; 3 | 4 | describe('encodeUrlPeriod と decodeUrlPeriod を使った処理のテスト', () => { 5 | test('エンコードしてデコードすると元のテキスト"a.b.c"を返す', () => { 6 | const original = 'a.b.c'; 7 | const encoded = encodeUrlPeriod(original); 8 | expect(encoded).not.toContain('.'); 9 | const decoded = decodeUrlPeriod(encoded); 10 | expect(decoded).toEqual(original); 11 | }); 12 | test('エンコードしてデコードすると元のテキスト"2E."を返す', () => { 13 | const original = '2E.'; 14 | const encoded = encodeUrlPeriod(original); 15 | expect(encoded).not.toContain('.'); 16 | const decoded = decodeUrlPeriod(encoded); 17 | expect(decoded).toEqual(original); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | type Props = { margin?: string }; 4 | 5 | export const Loading: React.FC = (props) => { 6 | return ; 7 | }; 8 | 9 | const rotate = keyframes` 10 | to { 11 | transform: rotate(360deg); 12 | } 13 | `; 14 | 15 | const fadein = keyframes` 16 | from { 17 | opacity: 0; 18 | } 19 | to { 20 | opacity: 1; 21 | } 22 | `; 23 | 24 | const StyledLoading = styled.div` 25 | display: table; 26 | width: 30px; 27 | height: 30px; 28 | margin: 0 auto; 29 | border: 4px solid var(--c-primary); 30 | border-radius: 50%; 31 | border-top-color: var(--c-primary-bg); 32 | animation: 33 | ${rotate} 0.8s linear infinite, 34 | ${fadein} 0.7s; 35 | `; 36 | -------------------------------------------------------------------------------- /packages/zenn-cli/src/client/components/PropertyRow.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | type Props = { 4 | title: string; 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const PropertyRow: React.FC = (props) => { 9 | return ( 10 | 11 |
{props.title}
12 |
{props.children}
13 |
14 | ); 15 | }; 16 | 17 | const StyledPropertyRow = styled.div` 18 | display: flex; 19 | font-size: 0.92rem; 20 | color: var(--c-gray); 21 | & + .property-row { 22 | margin-top: 0.5rem; 23 | } 24 | .property-row__title { 25 | font-weight: 700; 26 | width: 140px; 27 | } 28 | .property-row__content { 29 | flex: 1; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[typescriptreact]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[json]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[jsonc]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[yaml]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[css]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[scss]": { 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import { defineConfig, globalIgnores } from 'eslint/config'; 3 | import tseslint from 'typescript-eslint'; 4 | import eslintConfigPrettier from 'eslint-config-prettier/flat'; 5 | 6 | /** @type {import('eslint').Linter.Config} */ 7 | export default defineConfig( 8 | eslint.configs.recommended, 9 | tseslint.configs.recommended, 10 | 11 | { 12 | files: ['**/*.ts', '**/*.tsx'], 13 | rules: { 14 | '@typescript-eslint/no-require-imports': 'off', 15 | '@typescript-eslint/no-unused-vars': [ 16 | 'error', 17 | { 18 | argsIgnorePattern: '^_', 19 | varsIgnorePattern: '^_', 20 | caughtErrorsIgnorePattern: '^_', 21 | }, 22 | ], 23 | }, 24 | }, 25 | 26 | eslintConfigPrettier, 27 | 28 | globalIgnores(['**/*.js', '**/*.d.ts']) 29 | ); 30 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/src/utils/md-link-attributes.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | 3 | const markdownItLinkAttributes = require('markdown-it-link-attributes'); 4 | 5 | export function mdLinkAttributes(md: MarkdownIt) { 6 | // タグの属性を設定する 7 | // Ref: https://github.com/crookedneighbor/markdown-it-link-attributes 8 | md.use(markdownItLinkAttributes, [ 9 | // 内部リンク 10 | { 11 | matcher(href: string) { 12 | return href.match( 13 | /^(?:https:\/\/zenn\.dev$)|(?:https:\/\/zenn\.dev\/.*$)/ 14 | ); 15 | }, 16 | attrs: { 17 | target: '_blank', 18 | }, 19 | }, 20 | // 外部リンク 21 | { 22 | matcher(href: string) { 23 | return href.match(/^https?:\/\//); 24 | }, 25 | attrs: { 26 | target: '_blank', 27 | rel: 'nofollow noopener noreferrer', 28 | }, 29 | }, 30 | ]); 31 | } 32 | -------------------------------------------------------------------------------- /packages/zenn-cli/articles/301-example-embed-github.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '埋め込みのテスト(GitHub)' 3 | type: 'tech' # or "idea" 4 | topics: ['embed', 'test'] 5 | emoji: 🐲 6 | published: true 7 | --- 8 | 9 | ` 80 | -------------------------------------------------------------------------------- /packages/zenn-embed-elements/src/classes/katex.ts: -------------------------------------------------------------------------------- 1 | import { loadScript } from '../utils/load-script'; 2 | import { loadStylesheet } from '../utils/load-stylesheet'; 3 | 4 | declare let katex: any; 5 | 6 | const containerId = 'katex-container'; 7 | 8 | export class EmbedKatex extends HTMLElement { 9 | private _container: HTMLDivElement; 10 | 11 | constructor() { 12 | super(); 13 | const container = document.createElement('div'); 14 | container.setAttribute('id', containerId); 15 | this._container = container; 16 | } 17 | 18 | async connectedCallback() { 19 | this.render(); 20 | } 21 | 22 | async render() { 23 | if (typeof katex === 'undefined') { 24 | await loadScript({ 25 | // 本来はバージョンを指定した方がいいが、他の処理との兼ね合いでバージョンを固定するのは難しいので指定していません 26 | src: `https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js`, 27 | id: 'katex-js', 28 | }); 29 | } 30 | 31 | // CSSを読み込む(まだ読み込まれていない場合のみ) 32 | loadStylesheet({ 33 | // 本来はバージョンを指定した方がいいが、他の処理との兼ね合いでバージョンを固定するのは難しいので指定していません 34 | href: `https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css`, 35 | id: `katex-css`, 36 | }); 37 | 38 | const displayMode = !!this.getAttribute('display-mode'); 39 | 40 | // detailsタグの中ではinnerTextがnullになることがあるため 41 | const content = this.textContent || this.innerText; 42 | katex?.render(content, this._container, { 43 | macros: { '\\RR': '\\mathbb{R}' }, 44 | throwOnError: false, 45 | displayMode, 46 | }); 47 | this.innerHTML = this._container.innerHTML; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/ja/code-security/dependabot/working-with-dependabot/dependabot-options-reference 2 | 3 | version: 2 4 | updates: 5 | # root npm(pnpm)の依存関係 6 | - package-ecosystem: 'npm' # pnpmでもnpmと書く 7 | directory: '/' 8 | schedule: 9 | interval: 'monthly' 10 | cooldown: 11 | default-days: 21 12 | open-pull-requests-limit: 2 13 | groups: 14 | dev-dependencies: 15 | dependency-type: 'development' 16 | patterns: 17 | - '*' 18 | production-dependencies: 19 | dependency-type: 'production' 20 | patterns: 21 | - '*' 22 | commit-message: 23 | prefix: 'chore' 24 | prefix-development: 'chore' 25 | include: 'scope' 26 | labels: 27 | - 'dependencies' 28 | - 'automated' 29 | allow: 30 | - dependency-type: 'all' 31 | assignees: 32 | - 'cm-dyoshikawa' 33 | - 'cm-igarashi-ryosuke' 34 | - 'cm-wada-yusuke' 35 | 36 | # GitHub Actionsの依存関係 37 | - package-ecosystem: 'github-actions' 38 | directory: '/' 39 | schedule: 40 | interval: 'monthly' 41 | cooldown: 42 | default-days: 21 43 | open-pull-requests-limit: 1 44 | groups: 45 | all-actions: 46 | patterns: 47 | - '*' 48 | commit-message: 49 | prefix: 'chore' 50 | include: 'scope' 51 | labels: 52 | - 'automated' 53 | - 'dependencies' 54 | allow: 55 | - dependency-type: 'all' 56 | assignees: 57 | - 'cm-dyoshikawa' 58 | - 'cm-igarashi-ryosuke' 59 | - 'cm-wada-yusuke' 60 | -------------------------------------------------------------------------------- /packages/zenn-markdown-html/__tests__/custom-syntax/embed/youtube.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, test, expect } from 'vitest'; 2 | import { parse } from 'node-html-parser'; 3 | import markdownToHtml from '../../../src/index'; 4 | 5 | describe('Youtube埋め込み要素のテスト', () => { 6 | const validVideoId = 'exampletest'; // 11文字する 7 | const invalidVideoId = '@bad-video-id'; 8 | 9 | describe('デフォルトの挙動', () => { 10 | describe('有効なvideoIdの場合', () => { 11 | test('