Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions docs-site/content/.vuepress/components/CopyMarkdownButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
<template>
<div class="theme-default-content copy-markdown-button-wrapper">
<div class="button-group">
<button class="copy-markdown-button" @click="copyMarkdown" :class="{ copied: isCopied }" :title="buttonTitle">
<div class="icon-container">
<svg
class="copy-icon"
:class="{ hidden: isCopied }"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg
class="check-icon"
:class="{ visible: isCopied }"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<span>{{ buttonText }}</span>
</button>
<a v-if="markdownUrl" :href="markdownUrl" class="view-markdown-button" title="View raw markdown" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
</div>
</div>
</template>

<script>
export default {
name: 'CopyMarkdownButton',
data() {
return {
isCopied: false,
markdownContent: '',
copyTimeout: null,
}
},
computed: {
buttonText() {
return 'Copy Markdown'
},
buttonTitle() {
return this.isCopied ? 'Markdown copied to clipboard' : 'Copy markdown source to clipboard'
},
markdownUrl() {
return this.$page && this.$page.markdownUrl
},
},
watch: {
$route() {
if (this.copyTimeout) {
clearTimeout(this.copyTimeout)
this.copyTimeout = null
}
this.isCopied = false
},
},
beforeDestroy() {
if (this.copyTimeout) {
clearTimeout(this.copyTimeout)
}
},
methods: {
async copyMarkdown() {
if (this.copyTimeout) {
clearTimeout(this.copyTimeout)
}

this.isCopied = true

try {
const page = this.$page
if (!page || !page.markdown) {
throw new Error('Markdown content not available')
}

await navigator.clipboard.writeText(page.markdown)

this.copyTimeout = setTimeout(() => {
this.isCopied = false
this.copyTimeout = null
}, 2000)
} catch (error) {
console.error('Failed to copy markdown:', error)
this.isCopied = false
alert('Failed to copy markdown. Please try again.')
}
},
},
}
</script>

<style lang="stylus">
.copy-markdown-button-wrapper
margin-bottom -6.5rem !important
display flex
justify-content flex-end

.button-group
display flex
align-items stretch
border 1px solid #ddd
border-radius 4px
overflow hidden
background-color #f8f8f8

.copy-markdown-button
display inline-flex
align-items center
gap 0.5rem
padding 0.5rem 1rem
background-color transparent
border none
border-right 1px solid #ddd
color #2c3e50
font-size 0.875rem
font-weight 500
cursor pointer
transition all 0.2s ease

&:hover
background-color rgba(0, 0, 0, 0.04)

&:active
background-color rgba(0, 0, 0, 0.08)

&.copied
background-color #d4edda
color #155724

.icon-container
position relative
width 16px
height 16px
display flex
align-items center
justify-content center

svg
flex-shrink 0
position absolute
transition all 0.3s ease-out

.copy-icon
transform scale(1) rotate(0deg)
opacity 1

&.hidden
transform scale(0) rotate(90deg)
opacity 0

.check-icon
transform scale(0) rotate(-90deg)
opacity 0
stroke #155724

&.visible
transform scale(1) rotate(0deg)
opacity 1

.view-markdown-button
display inline-flex
align-items center
justify-content center
padding 0.5rem
background-color transparent
border none
color #2c3e50
cursor pointer
transition all 0.2s ease
text-decoration none

&:hover
background-color rgba(0, 0, 0, 0.04)

&:active
background-color rgba(0, 0, 0, 0.08)

svg
flex-shrink 0

@media (max-width: 719px)
.copy-markdown-button
font-size 0.8125rem
</style>
1 change: 1 addition & 0 deletions docs-site/content/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,7 @@ let config = {
'@vuepress/plugin-medium-zoom',
['@dovyp/vuepress-plugin-clipboard-copy', true],
require('./plugins/typesense-enhancements'),
require('./plugins/embed-markdown'),
[
'vuepress-plugin-sitemap',
{
Expand Down
111 changes: 111 additions & 0 deletions docs-site/content/.vuepress/plugins/embed-markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const fs = require('fs')
const path = require('path')

module.exports = (options, context) => ({
name: 'embed-markdown',

beforeDevServer(app) {
app.get('*.md', (req, res, next) => {
console.log('MD Request:', req.path)

let htmlPath = req.path.replace(/\.md$/, '.html')

if (req.path.endsWith('/README.md')) {
htmlPath = req.path.replace('/README.md', '/')
}

console.log('Looking for HTML path:', htmlPath)

const page = context.pages.find(p => p.path === htmlPath)
console.log('Found page:', page ? page.relativePath : 'NOT FOUND')

if (page && page.relativePath) {
try {
const sourceFile = path.join(context.sourceDir, page.relativePath)
console.log('Reading file:', sourceFile)
let markdown = fs.readFileSync(sourceFile, 'utf-8')
markdown = markdown.replace(/^---\n[\s\S]*?\n---\n*/m, '')

res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
res.send(markdown)
console.log('Served successfully')
} catch (error) {
console.error('Error:', error.message)
next()
}
} else {
console.log('Page not found, calling next()')
next()
}
})
},

extendPageData($page) {
if (!$page.relativePath || !$page.relativePath.endsWith('.md')) return

try {
const sourceFile = path.join(context.sourceDir, $page.relativePath)
let markdown = fs.readFileSync(sourceFile, 'utf-8')

markdown = markdown.replace(/^---\n[\s\S]*?\n---\n*/m, '')

$page.markdown = markdown

if ($page.path.endsWith('/')) {
$page.markdownUrl = `${$page.path}README.md`
} else {
$page.markdownUrl = $page.path.replace(/\.html$/, '.md')
}

if (!$page.frontmatter.head) {
$page.frontmatter.head = []
}
$page.frontmatter.head.push([
'link',
{
rel: 'alternate',
type: 'text/markdown',
href: $page.markdownUrl,
},
])
} catch (error) {
console.error(`Failed to embed markdown for ${$page.relativePath}:`, error.message)
}
},

async generated() {
const { outDir, pages, sourceDir } = context

for (const page of pages) {
if (!page.relativePath || !page.relativePath.endsWith('.md')) continue

try {
const sourceFile = path.join(sourceDir, page.relativePath)
let markdown = fs.readFileSync(sourceFile, 'utf-8')

markdown = markdown.replace(/^---\n[\s\S]*?\n---\n*/m, '')

let outputPath
if (page.path.endsWith('/')) {
outputPath = path.join(outDir, page.path, 'README.md')
} else {
outputPath = path.join(outDir, page.path.replace(/\.html$/, '.md'))
}

const outputDir = path.dirname(outputPath)

if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}

fs.writeFileSync(outputPath, markdown, 'utf-8')
} catch (error) {
console.error(`Failed to generate markdown for ${page.path}:`, error.message)
}
}

console.log(
`Generated ${pages.filter(p => p.relativePath && p.relativePath.endsWith('.md')).length} markdown files`,
)
},
})
25 changes: 25 additions & 0 deletions docs-site/content/.vuepress/theme/components/Page.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<main class="page">
<slot name="top" />

<CopyMarkdownButton />
<Content class="theme-default-content" />

<PageEdit />

<PageNav v-bind="{ sidebarItems }" />

<slot name="bottom" />
</main>
</template>

<script>
import PageEdit from '@parent-theme/components/PageEdit.vue'
import PageNav from '@parent-theme/components/PageNav.vue'
import CopyMarkdownButton from '../../components/CopyMarkdownButton.vue'

export default {
components: { PageEdit, PageNav, CopyMarkdownButton },
props: ['sidebarItems'],
}
</script>