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をもとに正規表現を定義した