From 1ab6cbfeb21220428c2e616dc12609435bb1b307 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Mon, 23 Dec 2024 11:34:33 +0800 Subject: [PATCH 1/9] fix: copy svg (#491) close #490 --- .../CodemirrorEditor/EditorHeader/index.vue | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/CodemirrorEditor/EditorHeader/index.vue b/src/components/CodemirrorEditor/EditorHeader/index.vue index c64249df1..6d8fdf403 100644 --- a/src/components/CodemirrorEditor/EditorHeader/index.vue +++ b/src/components/CodemirrorEditor/EditorHeader/index.vue @@ -64,6 +64,15 @@ const copyMode = useStorage(addPrefix(`copyMode`), `txt`) const source = ref(``) const { copy: copyContent } = useClipboard({ source }) +const creatEmptyNode = () => { + const node = document.createElement(`p`) + node.style.fontSize = `0` + node.style.lineHeight = `0` + node.style.margin = `0` + node.innerHTML = ` ` + return node +} + // 复制到微信公众号 function copy() { emit(`startCopy`) @@ -107,13 +116,11 @@ function copy() { clipboardDiv.focus() - // edge case: 由于 svg 无法复制, 在前面插入一个空节点 - const p = document.createElement(`p`) - p.style.fontSize = `0` // 设置字体大小为 0 - p.style.lineHeight = `0` // 行高也为 0 - p.style.margin = `0` // 避免外边距干扰 - p.innerHTML = ` ` - clipboardDiv.insertBefore(p, clipboardDiv.firstChild) + // 由于 svg 无法复制, 在前后各插入一个空白节点 + const beforeNode = creatEmptyNode() + const afterNode = creatEmptyNode() + clipboardDiv.insertBefore(beforeNode, clipboardDiv.firstChild) + clipboardDiv.appendChild(afterNode) // 兼容 Mermaid const nodes = clipboardDiv.querySelectorAll(`.nodeLabel`) From a9fa4c28240c1640117aade61e52e19949666910 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Mon, 23 Dec 2024 19:59:59 +0800 Subject: [PATCH 2/9] feat: build and deploy docker image (#495) --- .github/workflows/docker.yml | 39 +++++++++++ docker/latest/.env | 4 ++ docker/latest/Dockerfile.base | 14 ++++ docker/latest/Dockerfile.nginx | 6 ++ docker/latest/Dockerfile.standalone | 19 ++++++ docker/latest/Dockerfile.static | 12 ++++ docker/latest/patch/vite.config.ts | 66 +++++++++++++++++++ docker/latest/server/main.go | 21 ++++++ eslint.config.mjs | 2 +- package.json | 2 +- scripts/build-base-image.sh | 25 +++++++ scripts/build-nginx.sh | 25 +++++++ scripts/build-standalone.sh | 25 +++++++ scripts/build-static.sh | 25 +++++++ scripts/push-images.sh | 30 +++++++++ {bin => scripts}/release.js | 0 .../CodemirrorEditor/EditorHeader/index.vue | 2 +- 17 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 docker/latest/.env create mode 100644 docker/latest/Dockerfile.base create mode 100644 docker/latest/Dockerfile.nginx create mode 100644 docker/latest/Dockerfile.standalone create mode 100644 docker/latest/Dockerfile.static create mode 100644 docker/latest/patch/vite.config.ts create mode 100644 docker/latest/server/main.go create mode 100644 scripts/build-base-image.sh create mode 100644 scripts/build-nginx.sh create mode 100644 scripts/build-standalone.sh create mode 100644 scripts/build-static.sh create mode 100644 scripts/push-images.sh rename {bin => scripts}/release.js (100%) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..35d954a0a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,39 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build base image + run: bash scripts/build-base-image.sh + + - name: Build nginx image + run: bash scripts/build-nginx.sh + + - name: Build standalone image + run: bash scripts/build-standalone.sh + + - name: Build static image + run: bash scripts/build-static.sh + + - name: Push images to Docker Hub + run: bash scripts/push-images.sh diff --git a/docker/latest/.env b/docker/latest/.env new file mode 100644 index 000000000..947399743 --- /dev/null +++ b/docker/latest/.env @@ -0,0 +1,4 @@ +VER_APP=latest +VER_NGX=1.21.6-alpine +VER_GOLANG=1.17.6-alpine3.15 +VER_ALPINE=3.15 \ No newline at end of file diff --git a/docker/latest/Dockerfile.base b/docker/latest/Dockerfile.base new file mode 100644 index 000000000..f76a75eb9 --- /dev/null +++ b/docker/latest/Dockerfile.base @@ -0,0 +1,14 @@ +FROM node:20-alpine3.19 AS builder +ENV LANG="en_US.UTF-8" +ENV LANGUAGE="en_US.UTF-8" +ENV LC_ALL="en_US.UTF-8" +RUN apk add curl +RUN curl -L "https://github.com/doocs/md/archive/refs/heads/main.zip" -o "main.zip" && unzip "main.zip" && mv "md-main" /app +WORKDIR /app +COPY ./patch/vite.config.ts /app/vite.config.ts +ENV NODE_OPTIONS="--openssl-legacy-provider" +RUN npm i && npm run build + +FROM scratch +LABEL MAINTAINER="ylb" +COPY --from=builder /app/dist /app/assets diff --git a/docker/latest/Dockerfile.nginx b/docker/latest/Dockerfile.nginx new file mode 100644 index 000000000..b97106326 --- /dev/null +++ b/docker/latest/Dockerfile.nginx @@ -0,0 +1,6 @@ +ARG VER_NGX="1.21.6-alpine" + +FROM "doocs/md:latest-assets" AS assets +FROM "nginx:$VER_NGX" +LABEL MAINTAINER="ylb" +COPY --from=assets /app/* /usr/share/nginx/html diff --git a/docker/latest/Dockerfile.standalone b/docker/latest/Dockerfile.standalone new file mode 100644 index 000000000..755ec7869 --- /dev/null +++ b/docker/latest/Dockerfile.standalone @@ -0,0 +1,19 @@ +ARG VER_GOLANG=1.17.6-alpine3.15 +ARG VER_ALPINE=3.15 + +FROM "doocs/md:latest-assets" AS assets + +FROM "golang:$VER_GOLANG" AS gobuilder +COPY --from=assets /app/* /app/assets/ +COPY server/main.go /app +RUN apk add git bash gcc musl-dev upx +WORKDIR /app +RUN go build -ldflags "-w -s" -o md main.go && \ + apk add upx && \ + upx -9 -o md.minify md + +FROM "alpine:$VER_ALPINE" +LABEL MAINTAINER="ylb" +COPY --from=gobuilder /app/md.minify /bin/md +EXPOSE 80 +CMD ["md"] \ No newline at end of file diff --git a/docker/latest/Dockerfile.static b/docker/latest/Dockerfile.static new file mode 100644 index 000000000..c8ba628f9 --- /dev/null +++ b/docker/latest/Dockerfile.static @@ -0,0 +1,12 @@ +FROM doocs/md:latest-assets AS assets + +# detail https://github.com/lipanski/docker-static-website/blob/master/Dockerfile +FROM lipanski/docker-static-website + +WORKDIR /home/static + +COPY --from=assets /app/* /home/static + +EXPOSE 80 + +CMD ["/busybox-httpd", "-f", "-v", "-p", "80", "-c", "httpd.conf"] diff --git a/docker/latest/patch/vite.config.ts b/docker/latest/patch/vite.config.ts new file mode 100644 index 000000000..20eb26abb --- /dev/null +++ b/docker/latest/patch/vite.config.ts @@ -0,0 +1,66 @@ +import path from 'node:path' +import process from 'node:process' + +import vue from '@vitejs/plugin-vue' +import { visualizer } from 'rollup-plugin-visualizer' +import UnoCSS from 'unocss/vite' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { defineConfig } from 'vite' +import { nodePolyfills } from 'vite-plugin-node-polyfills' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: `/`, // 基本路径, 建议以绝对路径跟随访问目录 + define: { + process, + }, + plugins: [ + vue(), + UnoCSS(), + vueDevTools(), + nodePolyfills({ + include: [`path`, `util`, `timers`, `stream`, `fs`], + overrides: { + // Since `fs` is not supported in browsers, we can use the `memfs` package to polyfill it. + // fs: 'memfs', + }, + }), + process.env.ANALYZE === `true` && visualizer({ + emitFile: true, + filename: `stats.html`, + }), + AutoImport({ + imports: [ + `vue`, + `pinia`, + `@vueuse/core`, + ], + dirs: [ + `./src/stores`, + `./src/utils/toast`, + ], + }), + Components({ + resolvers: [], + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, `./src`), + }, + }, + css: { + devSourcemap: true, + }, + build: { + rollupOptions: { + output: { + chunkFileNames: `static/js/md-[name]-[hash].js`, + entryFileNames: `static/js/md-[name]-[hash].js`, + assetFileNames: `static/[ext]/md-[name]-[hash].[ext]`, + }, + }, + }, +}) diff --git a/docker/latest/server/main.go b/docker/latest/server/main.go new file mode 100644 index 000000000..f45d64601 --- /dev/null +++ b/docker/latest/server/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" +) + +//go:embed assets +var assets embed.FS + +func main() { + mutex := http.NewServeMux() + md, _ := fs.Sub(assets, "assets") + mutex.Handle("/", http.FileServer(http.FS(md))) + err := http.ListenAndServe(":80", mutex) + if err != nil { + log.Fatal(err) + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index e99a17789..ff4c663cd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ export default antfu({ unocss: true, typescript: true, formatters: true, - ignores: [`.github`, `bin`, `md-cli`, `src/assets`, `example`], + ignores: [`.github`, `scripts`, `docker`, `md-cli`, `src/assets`, `example`], }, { rules: { 'semi': [`error`, `never`], diff --git a/package.json b/package.json index 049170a53..2091a785d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:cli": "npm run build && npx shx rm -rf md-cli/dist && npx shx rm -rf dist/**/*.map && npx shx cp -r dist md-cli/ && cd md-cli && npm pack", "build:analyze": "cross-env ANALYZE=true vite build", "preview": "npm run build && vite preview", - "release:cli": "node ./bin/release.js", + "release:cli": "node ./scripts/release.js", "ext:dev": "wxt", "ext:zip": "wxt zip", "lint": "eslint . --fix", diff --git a/scripts/build-base-image.sh b/scripts/build-base-image.sh new file mode 100644 index 000000000..024cc3b78 --- /dev/null +++ b/scripts/build-base-image.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +RELEASE_DIR='./docker'; +REPO_NAME='doocs/md' + +for app_ver in $RELEASE_DIR/*; do + + if [ -f "$app_ver/Dockerfile.base" ]; then + + tag=$(echo $app_ver | cut -b 10-); + echo "Build: $tag"; + set -a + . "$app_ver/.env" + set +a + + echo $app_ver + echo "VER_APP: $VER_APP" + echo "VER_NGX: $VER_NGX" + echo "VER_GOLANG: $VER_GOLANG" + echo "VER_ALPINE: $VER_ALPINE" + + docker build --build-arg VER_APP=$VER_APP -f "$app_ver/Dockerfile.base" -t "$REPO_NAME:${VER_APP}-assets" "$app_ver" + fi + +done \ No newline at end of file diff --git a/scripts/build-nginx.sh b/scripts/build-nginx.sh new file mode 100644 index 000000000..b5a08fece --- /dev/null +++ b/scripts/build-nginx.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +RELEASE_DIR='./docker'; +REPO_NAME='doocs/md' + +for app_ver in $RELEASE_DIR/*; do + + if [ -f "$app_ver/Dockerfile.nginx" ]; then + + tag=$(echo $app_ver | cut -b 10-); + echo "Build: $tag"; + set -a + . "$app_ver/.env" + set +a + + echo $app_ver + echo "VER_APP: $VER_APP" + echo "VER_NGX: $VER_NGX" + echo "VER_GOLANG: $VER_GOLANG" + echo "VER_ALPINE: $VER_ALPINE" + + docker build --build-arg VER_APP=$VER_APP --build-arg VER_NGX=$VER_NGX -f "$app_ver/Dockerfile.nginx" -t "$REPO_NAME:${VER_APP}-nginx" "$app_ver" + fi + +done \ No newline at end of file diff --git a/scripts/build-standalone.sh b/scripts/build-standalone.sh new file mode 100644 index 000000000..0b011e104 --- /dev/null +++ b/scripts/build-standalone.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +RELEASE_DIR='./docker'; +REPO_NAME='doocs/md' + +for app_ver in $RELEASE_DIR/*; do + + if [ -f "$app_ver/Dockerfile.standalone" ]; then + + tag=$(echo $app_ver | cut -b 10-); + echo "Build: $tag"; + set -a + . "$app_ver/.env" + set +a + + echo $app_ver + echo "VER_APP: $VER_APP" + echo "VER_NGX: $VER_NGX" + echo "VER_GOLANG: $VER_GOLANG" + echo "VER_ALPINE: $VER_ALPINE" + + docker build --build-arg VER_APP=$VER_APP --build-arg VER_NGX=$VER_NGX -f "$app_ver/Dockerfile.standalone" -t "$REPO_NAME:${VER_APP}" "$app_ver" + fi + +done \ No newline at end of file diff --git a/scripts/build-static.sh b/scripts/build-static.sh new file mode 100644 index 000000000..36abd2a1c --- /dev/null +++ b/scripts/build-static.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +RELEASE_DIR='./docker'; +REPO_NAME='doocs/md' + +for app_ver in $RELEASE_DIR/*; do + + if [ -f "$app_ver/Dockerfile.static" ]; then + + tag=$(echo $app_ver | cut -b 10-); + echo "Build: $tag"; + set -a + . "$app_ver/.env" + set +a + + echo $app_ver + echo "VER_APP: $VER_APP" + echo "VER_NGX: $VER_NGX" + echo "VER_GOLANG: $VER_GOLANG" + echo "VER_ALPINE: $VER_ALPINE" + + docker build --build-arg VER_APP=$VER_APP --build-arg VER_NGX=$VER_NGX -f "$app_ver/Dockerfile.static" -t "$REPO_NAME:${VER_APP}-static" "$app_ver" + fi + +done \ No newline at end of file diff --git a/scripts/push-images.sh b/scripts/push-images.sh new file mode 100644 index 000000000..d38c4b645 --- /dev/null +++ b/scripts/push-images.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +RELEASE_DIR='./docker'; +REPO_NAME='doocs/md' + +for app_ver in $RELEASE_DIR/*; do + + tag=$(echo $app_ver | cut -b 10-); + + if [ -f "$app_ver/Dockerfile.base" ]; then + # 推送构建产物,方便其他的用户和爱好者进行二次封装 + docker push $REPO_NAME:$tag-assets + fi + + if [ -f "$app_ver/Dockerfile.standalone" ]; then + # 推送单个二进制的镜像 + docker push $REPO_NAME:$tag + fi + + if [ -f "$app_ver/Dockerfile.nginx" ]; then + # 推送使用 Nginx 的镜像 + docker push $REPO_NAME:$tag-nginx + fi + + if [ -f "$app_ver/Dockerfile.static" ]; then + # 推送使用 lipanski/docker-static-website 的镜像 + docker push $REPO_NAME:$tag-static + fi + +done \ No newline at end of file diff --git a/bin/release.js b/scripts/release.js similarity index 100% rename from bin/release.js rename to scripts/release.js diff --git a/src/components/CodemirrorEditor/EditorHeader/index.vue b/src/components/CodemirrorEditor/EditorHeader/index.vue index 6d8fdf403..009dbc162 100644 --- a/src/components/CodemirrorEditor/EditorHeader/index.vue +++ b/src/components/CodemirrorEditor/EditorHeader/index.vue @@ -64,7 +64,7 @@ const copyMode = useStorage(addPrefix(`copyMode`), `txt`) const source = ref(``) const { copy: copyContent } = useClipboard({ source }) -const creatEmptyNode = () => { +function creatEmptyNode() { const node = document.createElement(`p`) node.style.fontSize = `0` node.style.lineHeight = `0` From 03b0e73acb3531626adc070f51432d4db8392075 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Tue, 24 Dec 2024 08:46:21 +0800 Subject: [PATCH 3/9] chore: update copy func (#496) --- .../CodemirrorEditor/EditorHeader/index.vue | 294 ++++++++++-------- src/utils/index.ts | 74 ++++- 2 files changed, 241 insertions(+), 127 deletions(-) diff --git a/src/components/CodemirrorEditor/EditorHeader/index.vue b/src/components/CodemirrorEditor/EditorHeader/index.vue index 009dbc162..355587103 100644 --- a/src/components/CodemirrorEditor/EditorHeader/index.vue +++ b/src/components/CodemirrorEditor/EditorHeader/index.vue @@ -14,11 +14,26 @@ import { themeOptions, } from '@/config' import { useDisplayStore, useStore } from '@/stores' -import { addPrefix, mergeCss, solveWeChatImage } from '@/utils' -import { ChevronDownIcon, Moon, PanelLeftClose, PanelLeftOpen, Settings, Sun } from 'lucide-vue-next' +import { + addPrefix, + processClipboardContent, +} from '@/utils' +import { + ChevronDownIcon, + Moon, + PanelLeftClose, + PanelLeftOpen, + Settings, + Sun, +} from 'lucide-vue-next' import PickColors from 'vue-pick-colors' -const emit = defineEmits([`addFormat`, `formatContent`, `startCopy`, `endCopy`]) +const emit = defineEmits([ + `addFormat`, + `formatContent`, + `startCopy`, + `endCopy`, +]) const formatItems = [ { @@ -64,34 +79,10 @@ const copyMode = useStorage(addPrefix(`copyMode`), `txt`) const source = ref(``) const { copy: copyContent } = useClipboard({ source }) -function creatEmptyNode() { - const node = document.createElement(`p`) - node.style.fontSize = `0` - node.style.lineHeight = `0` - node.style.margin = `0` - node.innerHTML = ` ` - return node -} - // 复制到微信公众号 function copy() { emit(`startCopy`) setTimeout(() => { - function modifyHtmlStructure(htmlString: string) { - // 创建一个 div 元素来暂存原始 HTML 字符串 - const tempDiv = document.createElement(`div`) - tempDiv.innerHTML = htmlString - - const originalItems = tempDiv.querySelectorAll(`li > ul, li > ol`) - - originalItems.forEach((originalItem) => { - originalItem.parentElement!.insertAdjacentElement(`afterend`, originalItem) - }) - - // 返回修改后的 HTML 字符串 - return tempDiv.innerHTML - } - // 如果是深色模式,复制之前需要先切换到白天模式 const isBeforeDark = isDark.value if (isBeforeDark) { @@ -99,49 +90,11 @@ function copy() { } nextTick(async () => { - solveWeChatImage() - + processClipboardContent(primaryColor.value) const clipboardDiv = document.getElementById(`output`)! - clipboardDiv.innerHTML = mergeCss(clipboardDiv.innerHTML) - clipboardDiv.innerHTML = modifyHtmlStructure(clipboardDiv.innerHTML) - clipboardDiv.innerHTML = clipboardDiv.innerHTML - // 公众号不支持 position, 转换为等价的 translateY - .replace(/top:(.*?)em/g, `transform: translateY($1em)`) - // 适配主题中的颜色变量 - .replace(/hsl\(var\(--foreground\)\)/g, `#3f3f3f`) - .replace(/var\(--blockquote-background\)/g, `#f7f7f7`) - .replace(/var\(--md-primary-color\)/g, primaryColor.value) - .replace(/--md-primary-color:.+?;/g, ``) - .replace(/]*)>]*>(.*?)<\/p><\/span>/g, `$2`) - clipboardDiv.focus() - - // 由于 svg 无法复制, 在前后各插入一个空白节点 - const beforeNode = creatEmptyNode() - const afterNode = creatEmptyNode() - clipboardDiv.insertBefore(beforeNode, clipboardDiv.firstChild) - clipboardDiv.appendChild(afterNode) - - // 兼容 Mermaid - const nodes = clipboardDiv.querySelectorAll(`.nodeLabel`) - nodes.forEach((node) => { - const parent = node.parentElement! - const xmlns = parent.getAttribute(`xmlns`)! - const style = parent.getAttribute(`style`)! - const section = document.createElement(`section`) - section.setAttribute(`xmlns`, xmlns) - section.setAttribute(`style`, style) - section.innerHTML = parent.innerHTML - - const grand = parent.parentElement! - grand.innerHTML = `` - grand.appendChild(section) - }) - window.getSelection()!.removeAllRanges() - const temp = clipboardDiv.innerHTML - if (copyMode.value === `txt`) { const range = document.createRange() range.setStartBefore(clipboardDiv.firstChild!) @@ -150,19 +103,20 @@ function copy() { document.execCommand(`copy`) window.getSelection()!.removeAllRanges() } - clipboardDiv.innerHTML = output.value - if (isBeforeDark) { nextTick(() => toggleDark()) } - if (copyMode.value === `html`) { await copyContent(temp) } // 输出提示 - toast.success(copyMode.value === `html` ? `已复制 HTML 源码,请进行下一步操作。` : `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴。`) + toast.success( + copyMode.value === `html` + ? `已复制 HTML 源码,请进行下一步操作。` + : `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴。`, + ) editorRefresh() emit(`endCopy`) @@ -177,7 +131,9 @@ function customStyle() { }, 50) } -const pickColorsContainer = useTemplateRef(`pickColorsContainer`) +const pickColorsContainer = useTemplateRef( + `pickColorsContainer`, +) const format = ref(`rgb`) const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) @@ -191,18 +147,30 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) 格式 {{ label }} - + {{ item }} - + 微信外链转底部引用 @@ -212,10 +180,20 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) - - @@ -230,9 +208,14 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

主题

@@ -242,8 +225,13 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

字体

@@ -266,12 +259,19 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

主题色

@@ -325,16 +338,22 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

Mac 代码块

@@ -344,16 +363,22 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

微信外链转底部引用

@@ -363,16 +388,22 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

段落首行缩进

@@ -382,16 +413,23 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

自定义 CSS 面板

@@ -401,16 +439,22 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

编辑区位置

@@ -420,16 +464,22 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`])

模式

@@ -445,7 +495,9 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) -
+
@@ -456,11 +508,7 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) - + 公众号格式 diff --git a/src/utils/index.ts b/src/utils/index.ts index 89960878f..f354bd0a1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -340,20 +340,86 @@ export function removeLeft(str: string) { export function solveWeChatImage() { const clipboardDiv = document.getElementById(`output`)! const images = clipboardDiv.getElementsByTagName(`img`) - for (let i = 0; i < images.length; i++) { - const image = images[i] + + Array.from(images).forEach((image) => { const width = image.getAttribute(`width`)! const height = image.getAttribute(`height`)! image.removeAttribute(`width`) image.removeAttribute(`height`) image.style.width = width image.style.height = height - } + }) } -export function mergeCss(html: string) { +export function mergeCss(html: string): string { return juice(html, { inlinePseudoElements: true, preserveImportant: true, }) } + +export function createEmptyNode(): HTMLElement { + const node = document.createElement(`p`) + node.style.fontSize = `0` + node.style.lineHeight = `0` + node.style.margin = `0` + node.innerHTML = ` ` + return node +} + +export function modifyHtmlStructure(htmlString: string): string { + const tempDiv = document.createElement(`div`) + tempDiv.innerHTML = htmlString + + // 移动 `li > ul` 和 `li > ol` 到 `li` 后面 + tempDiv.querySelectorAll(`li > ul, li > ol`).forEach((originalItem) => { + originalItem.parentElement!.insertAdjacentElement(`afterend`, originalItem) + }) + + return tempDiv.innerHTML +} + +export function processClipboardContent(primaryColor: string) { + const clipboardDiv = document.getElementById(`output`)! + + // 先合并 CSS 和修改 HTML 结构 + clipboardDiv.innerHTML = modifyHtmlStructure(mergeCss(clipboardDiv.innerHTML)) + + // 处理样式和颜色变量 + clipboardDiv.innerHTML = clipboardDiv.innerHTML + .replace(/top:(.*?)em/g, `transform: translateY($1em)`) + .replace(/hsl\(var\(--foreground\)\)/g, `#3f3f3f`) + .replace(/var\(--blockquote-background\)/g, `#f7f7f7`) + .replace(/var\(--md-primary-color\)/g, primaryColor) + .replace(/--md-primary-color:.+?;/g, ``) + .replace( + /]*)>]*>(.*?)<\/p><\/span>/g, + `$2`, + ) + + // 处理图片大小 + solveWeChatImage() + + // 添加空白节点用于兼容 SVG 复制 + const beforeNode = createEmptyNode() + const afterNode = createEmptyNode() + clipboardDiv.insertBefore(beforeNode, clipboardDiv.firstChild) + clipboardDiv.appendChild(afterNode) + + // 兼容 Mermaid + const nodes = clipboardDiv.querySelectorAll(`.nodeLabel`) + nodes.forEach((node) => { + const parent = node.parentElement! + const xmlns = parent.getAttribute(`xmlns`)! + const style = parent.getAttribute(`style`)! + const section = document.createElement(`section`) + section.setAttribute(`xmlns`, xmlns) + section.setAttribute(`style`, style) + section.innerHTML = parent.innerHTML + + const grand = parent.parentElement! + // 清空父元素 + grand.innerHTML = `` + grand.appendChild(section) + }) +} From 23de6453a3d16b02040b5ab33cc6c92defdeb37b Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Tue, 24 Dec 2024 11:55:57 +0800 Subject: [PATCH 4/9] fix: render tab to spaces (#498) #497 --- src/utils/renderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index d07b27ec0..d461b36ff 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -174,6 +174,8 @@ export function initRenderer(opts: IOpts) { const langText = lang.split(` `)[0] const language = hljs.getLanguage(langText) ? langText : `plaintext` let highlighted = hljs.highlight(text, { language }).value + // tab to 4 spaces + highlighted = highlighted.replace(/\t/g, ' ') highlighted = highlighted .replace(/\r\n/g, `
`) .replace(/\n/g, `
`) From 13f5aad6aece413c4fcfba6b867ffabb00866ec0 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Sun, 29 Dec 2024 16:37:33 +0800 Subject: [PATCH 5/9] fix: deploy and push docker image (#500) --- .github/workflows/docker.yml | 2 +- .github/workflows/preview-build.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 35d954a0a..7478b1f61 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,7 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest - + if: github.repository == 'doocs/md' steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index f05cd482d..e4a3c5d73 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -8,7 +8,6 @@ jobs: build-preview: runs-on: ubuntu-latest if: github.repository == 'doocs/md' - steps: - uses: actions/checkout@v4 with: From 4f73811f0e4ced8e6b23528075b95e38f786218c Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Sun, 5 Jan 2025 21:34:14 +0800 Subject: [PATCH 6/9] fix: render codespan and front matter (#505) * fix: render codespan * style: update hr label * feat: support front matter --- package-lock.json | 51 +++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/config/theme.ts | 4 +++- src/stores/index.ts | 3 ++- src/utils/renderer.ts | 25 ++++++++++++++++++--- 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d0d3332c..d5ed4635c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "csstype": "^3.1.3", "es-toolkit": "^1.27.0", "form-data": "4.0.1", + "front-matter": "^4.0.2", "highlight.js": "^11.10.0", "juice": "^11.0.0", "lucide-vue-next": "^0.462.0", @@ -9217,6 +9218,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", @@ -9704,6 +9718,37 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1" + } + }, + "node_modules/front-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/front-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz", @@ -16856,6 +16901,12 @@ "node": ">=6" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.4.tgz", diff --git a/package.json b/package.json index 2091a785d..9d6ec78fe 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "csstype": "^3.1.3", "es-toolkit": "^1.27.0", "form-data": "4.0.1", + "front-matter": "^4.0.2", "highlight.js": "^11.10.0", "juice": "^11.0.0", "lucide-vue-next": "^0.462.0", diff --git a/src/config/theme.ts b/src/config/theme.ts index a19583ce1..fe7781864 100644 --- a/src/config/theme.ts +++ b/src/config/theme.ts @@ -206,12 +206,14 @@ const defaultTheme: Theme = { hr: { 'border-style': `solid`, - 'border-width': `1px 0 0`, + 'border-width': `2px 0 0`, 'border-color': `rgba(0,0,0,0.1)`, '-webkit-transform-origin': `0 0`, '-webkit-transform': `scale(1, 0.5)`, 'transform-origin': `0 0`, 'transform': `scale(1, 0.5)`, + 'height': `0.4em`, + 'margin': `1.5em 0`, }, }, inline: { diff --git a/src/stores/index.ts b/src/stores/index.ts index 75b97d230..14351c5a2 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -179,7 +179,8 @@ export const useStore = defineStore(`store`, () => { codeThemeChange() renderer.reset({ citeStatus: isCiteStatus.value, legend: legend.value, isUseIndent: isUseIndent.value }) - let outputTemp = marked.parse(editor.value!.getValue()) as string + const { markdownContent } = renderer.parseFrontMatterAndContent(editor.value!.getValue()) + let outputTemp = marked.parse(markdownContent) as string // 去除第一行的 margin-top outputTemp = outputTemp.replace(/(style=".*?)"/, `$1;margin-top: 0"`) diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index d461b36ff..75fa8d7ea 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -2,14 +2,18 @@ import type { ExtendedProperties, IOpts, ThemeStyles } from '@/types' import type { PropertiesHyphen } from 'csstype' import type { Renderer, RendererObject, Tokens } from 'marked' import { cloneDeep, toMerged } from 'es-toolkit' -import hljs from 'highlight.js' +import frontMatter from 'front-matter' +import hljs from 'highlight.js' import { marked } from 'marked' import mermaid from 'mermaid' import { getStyleString } from '.' import markedAlert from './MDAlert' import { MDKatex } from './MDKatex' +marked.setOptions({ + breaks: true, +}) marked.use(MDKatex({ nonStandard: true })) function buildTheme({ theme: _theme, fonts, size, isUseIndent }: IOpts): ThemeStyles { @@ -107,6 +111,19 @@ export function initRenderer(opts: IOpts) { return getStyles(styleMapping, tag, addition) } + function parseFrontMatterAndContent(markdownText: string) { + try { + const parsed = frontMatter(markdownText) + const yamlData = parsed.attributes + const markdownContent = parsed.body + return { yamlData, markdownContent } + } + catch (error) { + console.error(`Error parsing front-matter:`, error) + return { yamlData: {}, markdownContent: markdownText } + } + } + function styledContent(styleLabel: string, content: string, tagName?: string): string { const tag = tagName ?? styleLabel return `<${tag} ${styles(styleLabel)}>${content}` @@ -175,7 +192,7 @@ export function initRenderer(opts: IOpts) { const language = hljs.getLanguage(langText) ? langText : `plaintext` let highlighted = hljs.highlight(text, { language }).value // tab to 4 spaces - highlighted = highlighted.replace(/\t/g, ' ') + highlighted = highlighted.replace(/\t/g, ` `) highlighted = highlighted .replace(/\r\n/g, `
`) .replace(/\n/g, `
`) @@ -186,7 +203,8 @@ export function initRenderer(opts: IOpts) { }, codespan({ text }: Tokens.Codespan): string { - return styledContent(`codespan`, text, `code`) + const escapedText = text.replace(//g, `>`) + return styledContent(`codespan`, escapedText, `code`) }, listitem(item: Tokens.ListItem): string { @@ -276,6 +294,7 @@ export function initRenderer(opts: IOpts) { buildFootnotes, setOptions, reset, + parseFrontMatterAndContent, createContainer(content: string) { return styledContent(`container`, content, `section`) }, From 8f09acb06d3eacc62121aedc8f50153dcf321fe7 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Mon, 6 Jan 2025 09:28:52 +0800 Subject: [PATCH 7/9] fix: escape html (#506) --- src/utils/renderer.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 75fa8d7ea..23c962284 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -40,6 +40,16 @@ function buildTheme({ theme: _theme, fonts, size, isUseIndent }: IOpts): ThemeSt } as ThemeStyles } +function escapeHtml(text: string): string { + return text + .replace(/&/g, `&`) // 转义 & + .replace(//g, `>`) // 转义 > + .replace(/"/g, `"`) // 转义 " + .replace(/'/g, `'`) // 转义 ' + .replace(/`/g, ```) // 转义 ` +} + function buildAddition(): string { return `