diff --git a/packages/zenn-cli/articles/305-example-embed-others.md b/packages/zenn-cli/articles/305-example-embed-others.md
index b1fc833e..1d13f5f7 100644
--- a/packages/zenn-cli/articles/305-example-embed-others.md
+++ b/packages/zenn-cli/articles/305-example-embed-others.md
@@ -56,6 +56,12 @@ published: true
@[speakerdeck](4f926da9cb4cd0001f00a1ff)
+## docswell
+
+@[docswell](https://www.docswell.com/slide/LK7J5V/embed)
+
+@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell)
+
## jsfiddle
@[jsfiddle](https://jsfiddle.net/9wkngdue/embedded)
diff --git a/packages/zenn-content-css/src/_embed.scss b/packages/zenn-content-css/src/_embed.scss
index 91b199bd..ef10f705 100644
--- a/packages/zenn-content-css/src/_embed.scss
+++ b/packages/zenn-content-css/src/_embed.scss
@@ -8,6 +8,7 @@ span.embed-block {
}
.embed-slideshare,
.embed-speakerdeck,
+.embed-docswell,
.embed-codepen,
.embed-jsfiddle,
.embed-youtube,
diff --git a/packages/zenn-markdown-html/__tests__/custom-syntax/embed/docswell.test.ts b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/docswell.test.ts
new file mode 100644
index 00000000..fe47bd6c
--- /dev/null
+++ b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/docswell.test.ts
@@ -0,0 +1,35 @@
+import { describe, test, expect } from 'vitest';
+import markdownToHtml from '../../../src/index';
+
+describe('Docswell', () => {
+ describe('Docswellの埋め込み用URLの場合', () => {
+ test('Docswellのiframeを返すこと', () => {
+ const html = markdownToHtml(
+ '@[docswell](https://www.docswell.com/slide/LK7J5V/embed)'
+ );
+ expect(html).toContain(
+ ''
+ );
+ });
+ });
+
+ describe('DocswellのスライドURLの場合', () => {
+ test('Docswellのiframeを返すこと', () => {
+ const html = markdownToHtml(
+ '@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell)'
+ );
+ expect(html).toContain(
+ ''
+ );
+ });
+ });
+
+ describe('DocswellのURLが不正な場合', () => {
+ test('エラーメッセージを返すこと', () => {
+ const html = markdownToHtml(
+ '@[docswell](https://www.docswell.com/invalid)'
+ );
+ expect(html).toContain('DocswellのスライドURLが不正です');
+ });
+ });
+});
diff --git a/packages/zenn-markdown-html/__tests__/matchers/isDocswellUrl.test.ts b/packages/zenn-markdown-html/__tests__/matchers/isDocswellUrl.test.ts
new file mode 100644
index 00000000..22ec1f35
--- /dev/null
+++ b/packages/zenn-markdown-html/__tests__/matchers/isDocswellUrl.test.ts
@@ -0,0 +1,39 @@
+import { isDocswellUrl } from '../../src/utils/url-matcher';
+import { describe, test, expect } from 'vitest';
+
+describe('isDocswellUrlのテスト', () => {
+ describe('Docswellの埋め込み用URLの場合', () => {
+ test('trueを返すこと', () => {
+ const docswellEmbedUrl = 'https://www.docswell.com/slide/LK7J5V/embed';
+ expect(isDocswellUrl(docswellEmbedUrl)).toBe(true);
+ });
+ });
+
+ describe('DocswellのスライドURLの場合', () => {
+ test('trueを返すこと', () => {
+ const docswellSlideUrl =
+ 'https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell';
+ expect(isDocswellUrl(docswellSlideUrl)).toBe(true);
+ });
+ });
+
+ describe('Docswellの他の画面のURLの場合', () => {
+ test('falseを返すこと', () => {
+ const docswellUrls = ['https://www.docswell.com/'];
+
+ docswellUrls.forEach((url) => {
+ expect(isDocswellUrl(url)).toBe(false);
+ });
+ });
+ });
+
+ describe('他のサイトのURLの場合', () => {
+ test('falseを返すこと', () => {
+ const otherSiteUrls = ['https://zenn.dev/', 'https://github.com/'];
+
+ otherSiteUrls.forEach((url) => {
+ expect(isDocswellUrl(url)).toBe(false);
+ });
+ });
+ });
+});
diff --git a/packages/zenn-markdown-html/src/embed.ts b/packages/zenn-markdown-html/src/embed.ts
index 50e400e8..4d25b6be 100644
--- a/packages/zenn-markdown-html/src/embed.ts
+++ b/packages/zenn-markdown-html/src/embed.ts
@@ -1,7 +1,6 @@
import type { MarkdownOptions } from './types';
import { escapeHtml } from 'markdown-it/lib/common/utils';
-import { extractYoutubeVideoParameters } from './utils/url-matcher';
import {
sanitizeEmbedToken,
generateEmbedServerIframe,
@@ -17,6 +16,9 @@ import {
isBlueprintUEUrl,
isFigmaUrl,
isValidHttpUrl,
+ isDocswellUrl,
+ extractYoutubeVideoParameters,
+ extractDocswellEmbedUrl,
} from './utils/url-matcher';
/* 埋め込み要素の種別 */
@@ -25,6 +27,7 @@ export type EmbedType =
| 'slideshare'
| 'speakerdeck'
| 'jsfiddle'
+ | 'docswell'
| 'codepen'
| 'codesandbox'
| 'stackblitz'
@@ -77,6 +80,17 @@ export const embedGenerators: Readonly = {
key
)}" scrolling="no" allowfullscreen allow="encrypted-media" loading="lazy">`;
},
+ docswell(str) {
+ const errorMessage = 'DocswellのスライドURLが不正です';
+ if (!isDocswellUrl(str)) {
+ return errorMessage;
+ }
+ const slideUrl = extractDocswellEmbedUrl(str);
+ if (!slideUrl) {
+ return errorMessage;
+ }
+ return ``;
+ },
jsfiddle(str) {
if (!isJsfiddleUrl(str)) {
return 'jsfiddleのURLが不正です';
diff --git a/packages/zenn-markdown-html/src/utils/url-matcher.ts b/packages/zenn-markdown-html/src/utils/url-matcher.ts
index 73ac988d..b2be34ec 100644
--- a/packages/zenn-markdown-html/src/utils/url-matcher.ts
+++ b/packages/zenn-markdown-html/src/utils/url-matcher.ts
@@ -47,6 +47,17 @@ export function isJsfiddleUrl(url: string): boolean {
return /^(http|https):\/\/jsfiddle\.net\/[a-zA-Z0-9_,/-]+$/.test(url);
}
+const docswellNormalUrlRegex =
+ /^https:\/\/www\.docswell\.com\/s\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
+const docswellEmbedUrlRegex =
+ /^https:\/\/www\.docswell\.com\/slide\/[a-zA-Z0-9_-]+\/embed$/;
+
+export function isDocswellUrl(url: string): boolean {
+ return [docswellNormalUrlRegex, docswellEmbedUrlRegex].some((pattern) =>
+ pattern.test(url)
+ );
+}
+
export function isYoutubeUrl(url: string): boolean {
return [
/^https?:\/\/youtu\.be\/[\w-]+(?:\?[\w=&-]+)?$/,
@@ -80,6 +91,22 @@ export function extractYoutubeVideoParameters(
return { videoId, start };
}
+export function extractDocswellEmbedUrl(url: string): string | null {
+ // Embed用URLの場合、そのまま返す
+ if (docswellEmbedUrlRegex.test(url)) {
+ return url;
+ }
+ // Embed用URLでない場合 https://www.docswell.com/s/:username/{slideId}-hello-docswell のslideIdを抽出する
+ const slideId = new URL(url).pathname.split('/').at(3)?.split('-').at(0);
+ if (!slideId) {
+ return null;
+ }
+ return new URL(
+ `/slide/${slideId}/embed`,
+ 'https://www.docswell.com'
+ ).toString();
+}
+
/**
* 参考: https://blueprintue.com/
* 生成されるURLをもとに正規表現を定義した