Skip to content
Merged
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
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ Image. Supports uploading attachments

## Installation

Install packages
Install from npm:

```sh
pnpm install
npm i -g markdown2conf
mdconf -V
```

Optionally install executable globally as `mdconf`
Install from source code:

```sh
pnpm build
pnpm link --global
mdconf -V
```

## Usage
Expand All @@ -49,15 +51,15 @@ Create a config file at `~/.config/mdconf.json`
### Convert Markdown to Confluence markup

```sh
pnpm dev test/demo.md <output>
mdconf test/demo.md <output>
```

If `output` is provided, saves the result to a file. Otherwise, prints to stdout.

### Extract frontmatter (title, labels)

```sh
pnpm dev frontmatter test/demo.md
mdconf frontmatter test/demo.md
```

```json
Expand Down Expand Up @@ -86,7 +88,7 @@ mdconf publish markdown.md -i <id> -m 'message'
### Help

```sh
pnpm dev --help
mdconf --help
```

```text
Expand All @@ -96,17 +98,17 @@ Usage: mdconf [options] [command] <input.md> [output.confluence]
Markdown to Confluence Wiki Markup Converter

Arguments:
input.md Markdown input file
output.confluence Output file (optional)
input.md Markdown input file
output.confluence Output file (optional)

Options:
-V, --version output the version number
-h, --help display help for command
-V, --version output the version number
-h, --help display help for command

Commands:
frontmatter <input.md> Extract frontmatter (title, labels)
publish [options] <markdown.md> Convert markdown/markup to storage format and publish to Confluence page
new [options] <markdown.md> Create a new Confluence page from markdown with frontmatter
frontmatter <input.md> Extract frontmatter (id, title, labels)
publish|pub [options] <markdown.md> Convert markdown/markup to storage format and publish to Confluence page
new [options] <markdown.md> Create a new Confluence page from markdown with frontmatter
```

## Example
Expand Down
40 changes: 29 additions & 11 deletions api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,23 +192,34 @@ async function getAttachment(pageId: number): Promise<string[]> {
}

async function addAttachment(pageId: number, filePath: string, comment: string): Promise<any> {
const api = `rest/api/content/${pageId}/child/attachment`
const { confluenceToken: token } = await loadConfig()

const filename = filePath
.split('/')
.pop()
?.replace(/[^a-zA-Z0-9.-]/g, '_') // Sanitize filename

const { confluenceToken: token, host } = await loadConfig()
const filename = path.basename(filePath)
const fileBuffer = await fs.readFile(filePath)
const uint8Array = new Uint8Array(fileBuffer)
const blob = new Blob([uint8Array], { type: 'image/png' })

const formData = new FormData()
formData.append('comment', comment)
formData.append('file', blob, filename)
formData.append('minorEdit', 'true')

const response = await fetch(api, {
const id = await getAttachmentByName(pageId, filename)
if (id !== null) {
console.log(`Attachment ${filename} already exists. Updating it...`)
const response = await fetch(`${host}/rest/api/content/${pageId}/child/attachment/${id}/data`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'X-Atlassian-Token': 'nocheck', // Attachment upload requires this header
},
body: formData,
})
if (!response.ok) {
const text = await response.text()
throw new Error(`updating attachment "${filename}" on page ${pageId}: ${response.status} ${text}`)
}
return response.json()
}
const response = await fetch(`${host}/rest/api/content/${pageId}/child/attachment`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -218,11 +229,17 @@ async function addAttachment(pageId: number, filePath: string, comment: string):
})
if (!response.ok) {
const text = await response.text()
throw new Error(`Request failed ${pageId}: ${response.status} ${text}`)
throw new Error(`creating attachment "${filename}" on page ${pageId}: ${response.status} ${text}`)
}
return response.json()
}

async function getAttachmentByName(pageId: number, filename: string): Promise<number | null> {
const api = `rest/api/content/${pageId}/child/attachment?filename=${encodeURIComponent(filename)}`
const data = await request('GET', api)
return data.results[0]?.id || null
}

async function postLabels(pageId: number, labels: string[]): Promise<any> {
const api = `rest/api/content/${pageId}/label`
const body = labels.map((label) => ({ prefix: 'global', name: label.toLowerCase() }))
Expand Down Expand Up @@ -259,6 +276,7 @@ async function syncLabels(pageId: number, labels: string[]): Promise<void> {
}
}


export {
addAttachment,
createPage,
Expand Down
5 changes: 3 additions & 2 deletions convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,9 @@ const confluenceRenderer = {
let src = href
const isUrl = /^https?:\/\//.test(href)
if (!isUrl) {
src = src.split('/').pop().split('?')[0]
localImages.push(href)
// For local images, decode URL encoding and get just the filename for Confluence display
src = decodeURIComponent(src.split('/').pop().split('?')[0])
localImages.push(href) // Keep original href for file path resolution
}
if (text && text.trim()) {
return `!${src}|alt=${text.trim()}!`
Expand Down
8 changes: 4 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createPage, homePage, markupToStorage, syncLabels } from './api.js'
import { convertToConfluence, extractFrontMatter } from './convert.js'
import { inferPageId, mdToStorage, relativePaths, updateConfluencePage, upploadImages } from './main.js'

const VERSION = '1.4.12-rc.1'
const VERSION = '1.4.13-rc.1'

async function main() {
const program = new Command()
Expand Down Expand Up @@ -76,7 +76,7 @@ async function main() {
await updateConfluencePage({ pageId: pageIdNum, storage, message, labels, title: newTitle })
if (options.attachment && localImages.length > 0) {
const relativeImagePaths = relativePaths(filePath, localImages)
upploadImages(pageIdNum, relativeImagePaths)
await upploadImages(pageIdNum, relativeImagePaths)
}
}
} catch (error) {
Expand Down Expand Up @@ -105,11 +105,11 @@ async function main() {
console.log(`Created new Confluence page:\n${result._links.base + result._links.tinyui}`)
if (localImages.length > 0) {
const relativeImagePaths = relativePaths(filePath, localImages)
upploadImages(result.id, relativeImagePaths)
await upploadImages(result.id, relativeImagePaths)
}
const attrs = await extractFrontMatter(await fs.readFile(filePath, 'utf-8'))
if (attrs.labels && attrs.labels.length > 0) {
syncLabels(result.id, attrs.labels)
await syncLabels(result.id, attrs.labels)
}
} catch (error) {
console.error(`${error.message}`)
Expand Down
51 changes: 28 additions & 23 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ function inferPageId(srcFile: string): number {
}

interface UpdatePageParams {
pageId: number
storage: string
message: string
labels?: string[]
title?: string
pageId: number
storage: string
message: string
labels?: string[]
title?: string
}

async function updateConfluencePage(params: UpdatePageParams) {
const { pageId, storage, message, title, labels = [] } = params
const page = await getPage(pageId)
if (labels.length > 0) {
syncLabels(pageId, labels)
}
const pageTitle = title || page.title
const result = await editPage(pageId, storage, pageTitle, page.version + 1, page.space, message)
console.log(`Published to Confluence: version ${result.version.number}\n${page.tinyui}`)
const { pageId, storage, message, title, labels = [] } = params
const page = await getPage(pageId)
if (labels.length > 0) {
await syncLabels(pageId, labels)
}
const pageTitle = title || page.title
const result = await editPage(pageId, storage, pageTitle, page.version + 1, page.space, message)
console.log(`Published to Confluence: version ${result.version.number}\n${page.tinyui}`)
}

async function mdToStorage(markdownPath: string, options: { title?: string }) {
Expand All @@ -44,15 +44,20 @@ async function mdToStorage(markdownPath: string, options: { title?: string }) {
}

async function upploadImages(pageId: number, imagePaths: string[]): Promise<any[]> {
return imagePaths.map((imagePath) => {
console.log(`Uploading image: ${imagePath}`)
try {
return addAttachment(pageId, imagePath, 'Uploaded by mdconf')
} catch (error) {
console.error(`Failed to upload ${imagePath}: ${error.message}`)
return null
}
})
const results: any[] = []

for (const imagePath of imagePaths) {
console.info(`Uploading image: ${imagePath}`)
try {
const result = await addAttachment(pageId, imagePath, 'Uploaded by mdconf')
results.push(result)
} catch (error) {
console.error(`Failed to upload ${imagePath}: ${error.message}`)
results.push(null)
}
}

return results
}

function relativePaths(basePath: string, imagePaths: string[]): string[] {
Expand All @@ -64,7 +69,7 @@ function relativePaths(basePath: string, imagePaths: string[]): string[] {
if (path.isAbsolute(imgPath)) {
return imgPath
}
return path.resolve(baseDir, imgPath)
return path.resolve(baseDir, decodeURIComponent(imgPath))
})
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "markdown2conf",
"version": "1.4.12-rc.1",
"version": "1.4.13-rc.1",
"type": "module",
"description": "Convert Markdown files to Confluence Wiki Markup and publish to Confluence with CLI",
"main": "index.js",
Expand Down
7 changes: 7 additions & 0 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ h5. heading 5`)
expect(got.trim()).toBe(want)
})

it('converts local images with URI encoded paths', async () => {
const md = '![alt text](./image%20name.png)\n![](https://example.org/image%20name.png)'
const { markup: got } = await convertToConfluence(md)
const want = '!image name.png|alt=alt text!\n!https://example.org/image%20name.png!'
expect(got.trim()).toBe(want)
})

it('converts unordered lists', async () => {
const md = '- item1\n- item2'
const { markup: conf } = await convertToConfluence(md)
Expand Down
2 changes: 1 addition & 1 deletion test/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ New line
[#Test Lists](#test-lists)
[#Heading-With Dash](#heading-with-dash)

![Example Alt](./example.jpg)
![Example Alt](./example%201.jpg)

## heading 2

Expand Down