From e184412fdab0e34a7c10c60e85798a116f2610ae Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:42:24 +0900 Subject: [PATCH 1/6] URI decode and encode file path --- api.ts | 5 +---- main.ts | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/api.ts b/api.ts index b2e6a13..4e09d88 100644 --- a/api.ts +++ b/api.ts @@ -195,10 +195,7 @@ async function addAttachment(pageId: number, filePath: string, comment: string): 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 filename = encodeURIComponent(path.basename(filePath)) const fileBuffer = await fs.readFile(filePath) const uint8Array = new Uint8Array(fileBuffer) diff --git a/main.ts b/main.ts index 9c1372b..7434650 100644 --- a/main.ts +++ b/main.ts @@ -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) { + 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 }) { @@ -64,7 +64,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)) }) } From 6edf5ba732b0bd5603a01f8415f7c2a71fcb8984 Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:15:17 +0900 Subject: [PATCH 2/6] fix confluence attachment duplicate name --- api.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/api.ts b/api.ts index 4e09d88..5d678f8 100644 --- a/api.ts +++ b/api.ts @@ -192,11 +192,8 @@ async function getAttachment(pageId: number): Promise { } async function addAttachment(pageId: number, filePath: string, comment: string): Promise { - const api = `rest/api/content/${pageId}/child/attachment` - const { confluenceToken: token } = await loadConfig() - + const { confluenceToken: token, host } = await loadConfig() const filename = encodeURIComponent(path.basename(filePath)) - const fileBuffer = await fs.readFile(filePath) const uint8Array = new Uint8Array(fileBuffer) const blob = new Blob([uint8Array], { type: 'image/png' }) @@ -204,8 +201,26 @@ async function addAttachment(pageId: number, filePath: string, comment: string): 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(`Request failed ${pageId}: ${response.status} ${text}`) + } + return response.json() + } + const response = await fetch(`${host}/rest/api/content/${pageId}/child/attachment`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, @@ -220,6 +235,12 @@ async function addAttachment(pageId: number, filePath: string, comment: string): return response.json() } +async function getAttachmentByName(pageId: number, filename: string): Promise { + 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 { const api = `rest/api/content/${pageId}/label` const body = labels.map((label) => ({ prefix: 'global', name: label.toLowerCase() })) @@ -256,6 +277,7 @@ async function syncLabels(pageId: number, labels: string[]): Promise { } } + export { addAttachment, createPage, From ff6e982de186c7c2b109f482283634583202b821 Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:30:57 +0900 Subject: [PATCH 3/6] decode URI encoded local image paths --- api.ts | 3 +-- convert.js | 5 +++-- test/basic.test.js | 7 +++++++ test/demo.md | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/api.ts b/api.ts index 5d678f8..c9b6493 100644 --- a/api.ts +++ b/api.ts @@ -193,11 +193,10 @@ async function getAttachment(pageId: number): Promise { async function addAttachment(pageId: number, filePath: string, comment: string): Promise { const { confluenceToken: token, host } = await loadConfig() - const filename = encodeURIComponent(path.basename(filePath)) + 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) diff --git a/convert.js b/convert.js index 818f603..b9d7f7b 100644 --- a/convert.js +++ b/convert.js @@ -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()}!` diff --git a/test/basic.test.js b/test/basic.test.js index 73a0947..b0e037e 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -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) diff --git a/test/demo.md b/test/demo.md index 0a20a9c..e1a949f 100644 --- a/test/demo.md +++ b/test/demo.md @@ -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 From 1e32976b6fddbae41434dc2539b46008c8215d8d Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:39:32 +0900 Subject: [PATCH 4/6] fix confluence attachment rate limit --- api.ts | 4 ++-- main.ts | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/api.ts b/api.ts index c9b6493..aa270db 100644 --- a/api.ts +++ b/api.ts @@ -215,7 +215,7 @@ 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(`updating attachment "${filename}" on page ${pageId}: ${response.status} ${text}`) } return response.json() } @@ -229,7 +229,7 @@ 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() } diff --git a/main.ts b/main.ts index 7434650..11ef7dd 100644 --- a/main.ts +++ b/main.ts @@ -44,15 +44,20 @@ async function mdToStorage(markdownPath: string, options: { title?: string }) { } async function upploadImages(pageId: number, imagePaths: string[]): Promise { - 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[] { From 9cba8859f1d58549a2557cb08c1eafca5cae49d3 Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:46:25 +0900 Subject: [PATCH 5/6] add awaits --- index.ts | 6 +++--- main.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 5ccdde4..6fdfc22 100644 --- a/index.ts +++ b/index.ts @@ -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) { @@ -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}`) diff --git a/main.ts b/main.ts index 11ef7dd..3df990e 100644 --- a/main.ts +++ b/main.ts @@ -24,7 +24,7 @@ async function updateConfluencePage(params: UpdatePageParams) { const { pageId, storage, message, title, labels = [] } = params const page = await getPage(pageId) if (labels.length > 0) { - syncLabels(pageId, labels) + await syncLabels(pageId, labels) } const pageTitle = title || page.title const result = await editPage(pageId, storage, pageTitle, page.version + 1, page.space, message) From f872b6b64c8692145b2b061f644e473b526d1298 Mon Sep 17 00:00:00 2001 From: iucario <16816842+iucario@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:34:39 +0900 Subject: [PATCH 6/6] update version and README --- README.md | 28 +++++++++++++++------------- index.ts | 2 +- package.json | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 283b48c..5e08ce2 100644 --- a/README.md +++ b/README.md @@ -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 @@ -49,7 +51,7 @@ Create a config file at `~/.config/mdconf.json` ### Convert Markdown to Confluence markup ```sh -pnpm dev test/demo.md +mdconf test/demo.md ``` If `output` is provided, saves the result to a file. Otherwise, prints to stdout. @@ -57,7 +59,7 @@ 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 @@ -86,7 +88,7 @@ mdconf publish markdown.md -i -m 'message' ### Help ```sh -pnpm dev --help +mdconf --help ``` ```text @@ -96,17 +98,17 @@ Usage: mdconf [options] [command] [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 Extract frontmatter (title, labels) - publish [options] Convert markdown/markup to storage format and publish to Confluence page - new [options] Create a new Confluence page from markdown with frontmatter + frontmatter Extract frontmatter (id, title, labels) + publish|pub [options] Convert markdown/markup to storage format and publish to Confluence page + new [options] Create a new Confluence page from markdown with frontmatter ``` ## Example diff --git a/index.ts b/index.ts index 6fdfc22..7be4560 100644 --- a/index.ts +++ b/index.ts @@ -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() diff --git a/package.json b/package.json index 8bd1c40..1f8b463 100644 --- a/package.json +++ b/package.json @@ -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",