tdecollab 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,16 +4,21 @@ import {
4
4
 
5
5
  // tools/common/env-loader.ts
6
6
  import dotenv from "dotenv";
7
+ import fs from "fs";
7
8
  import path from "path";
8
9
  import os from "os";
9
10
  var loaded = false;
11
+ var lastResult = null;
10
12
  function getHomeDir() {
11
13
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
12
14
  }
13
15
  function loadEnv() {
14
- const result = { loadedFiles: [], skippedFiles: [] };
15
- if (loaded) return result;
16
+ if (loaded && lastResult) return lastResult;
17
+ const result = { loadedFiles: [], skippedFiles: [], sources: {} };
16
18
  loaded = true;
19
+ for (const key of Object.keys(process.env)) {
20
+ result.sources[key] = "shell env";
21
+ }
17
22
  const candidates = [
18
23
  // 우선순위 2: 현재 디렉토리
19
24
  path.resolve(process.cwd(), "tdecollab.env"),
@@ -21,15 +26,31 @@ function loadEnv() {
21
26
  path.join(getHomeDir(), ".config", "tdecollab", ".env")
22
27
  ];
23
28
  for (const filepath of candidates) {
29
+ let parsed = {};
30
+ try {
31
+ parsed = dotenv.parse(fs.readFileSync(filepath, "utf-8"));
32
+ } catch {
33
+ }
34
+ const beforeKeys = new Set(Object.keys(process.env));
24
35
  const out = dotenv.config({ path: filepath, override: false });
25
36
  if (out.error) {
26
37
  result.skippedFiles.push(filepath);
27
38
  } else {
28
39
  result.loadedFiles.push(filepath);
40
+ for (const key of Object.keys(parsed)) {
41
+ if (!beforeKeys.has(key) && process.env[key] !== void 0) {
42
+ result.sources[key] = filepath;
43
+ }
44
+ }
29
45
  }
30
46
  }
47
+ lastResult = result;
31
48
  return result;
32
49
  }
50
+ function getEnvSource(key) {
51
+ const result = loadEnv();
52
+ return result.sources[key] || "<unset>";
53
+ }
33
54
 
34
55
  // tools/confluence/api/content.ts
35
56
  var ConfluenceContentApi = class {
@@ -38,14 +59,14 @@ var ConfluenceContentApi = class {
38
59
  }
39
60
  async getPage(id, expand) {
40
61
  const expandParam = expand ? expand.join(",") : "body.storage,version,space,metadata.labels";
41
- const response = await this.client.get(`/rest/api/content/${id}`, {
62
+ const response = await this.client.get(`rest/api/content/${id}`, {
42
63
  params: { expand: expandParam }
43
64
  });
44
65
  return response.data;
45
66
  }
46
67
  async getPageByTitle(spaceKey, title, expand) {
47
68
  const expandParam = expand ? expand.join(",") : "body.storage,version,space";
48
- const response = await this.client.get("/rest/api/content", {
69
+ const response = await this.client.get("rest/api/content", {
49
70
  params: {
50
71
  spaceKey,
51
72
  title,
@@ -73,7 +94,7 @@ var ConfluenceContentApi = class {
73
94
  if (params.parentId) {
74
95
  data.ancestors = [{ id: params.parentId }];
75
96
  }
76
- const response = await this.client.post("/rest/api/content", data);
97
+ const response = await this.client.post("rest/api/content", data);
77
98
  if (params.labels && params.labels.length > 0) {
78
99
  await this.addLabels(response.data.id, params.labels);
79
100
  }
@@ -91,14 +112,14 @@ var ConfluenceContentApi = class {
91
112
  }
92
113
  }
93
114
  };
94
- const response = await this.client.put(`/rest/api/content/${params.id}`, data);
115
+ const response = await this.client.put(`rest/api/content/${params.id}`, data);
95
116
  return response.data;
96
117
  }
97
118
  async deletePage(id) {
98
- await this.client.delete(`/rest/api/content/${id}`);
119
+ await this.client.delete(`rest/api/content/${id}`);
99
120
  }
100
121
  async getChildPages(id, start = 0, limit = 25) {
101
- const response = await this.client.get(`/rest/api/content/${id}/child/page`, {
122
+ const response = await this.client.get(`rest/api/content/${id}/child/page`, {
102
123
  params: { start, limit }
103
124
  });
104
125
  return response.data.results;
@@ -112,7 +133,7 @@ var ConfluenceContentApi = class {
112
133
  // For now, simple implementation to support createPage.
113
134
  async addLabels(id, labels) {
114
135
  const data = labels.map((name) => ({ prefix: "global", name }));
115
- await this.client.post(`/rest/api/content/${id}/label`, data);
136
+ await this.client.post(`rest/api/content/${id}/label`, data);
116
137
  }
117
138
  // Attachment 관련 메서드 (upsert: 기존 파일이 있으면 업데이트, 없으면 신규 업로드)
118
139
  async uploadAttachment(pageId, filename, fileContent, contentType) {
@@ -132,13 +153,13 @@ var ConfluenceContentApi = class {
132
153
  let response;
133
154
  if (existing) {
134
155
  response = await this.client.post(
135
- `/rest/api/content/${pageId}/child/attachment/${existing.id}/data`,
156
+ `rest/api/content/${pageId}/child/attachment/${existing.id}/data`,
136
157
  form,
137
158
  { headers }
138
159
  );
139
160
  } else {
140
161
  response = await this.client.post(
141
- `/rest/api/content/${pageId}/child/attachment`,
162
+ `rest/api/content/${pageId}/child/attachment`,
142
163
  form,
143
164
  { headers }
144
165
  );
@@ -149,7 +170,7 @@ var ConfluenceContentApi = class {
149
170
  return response.data;
150
171
  }
151
172
  async getAttachments(pageId, filename) {
152
- const response = await this.client.get(`/rest/api/content/${pageId}/child/attachment`, {
173
+ const response = await this.client.get(`rest/api/content/${pageId}/child/attachment`, {
153
174
  params: {
154
175
  filename,
155
176
  expand: "version"
@@ -171,7 +192,7 @@ var ConfluenceSpaceApi = class {
171
192
  this.client = client;
172
193
  }
173
194
  async getSpaces(type = "global", start = 0, limit = 25) {
174
- const response = await this.client.get("/rest/api/space", {
195
+ const response = await this.client.get("rest/api/space", {
175
196
  params: { type, start, limit }
176
197
  });
177
198
  return response.data.results;
@@ -189,7 +210,7 @@ var ConfluenceSearchApi = class {
189
210
  }
190
211
  async searchByCql(cql, start = 0, limit = 25, expand) {
191
212
  const expandParam = expand ? expand.join(",") : "body.storage,version,space,metadata.labels";
192
- const response = await this.client.get("/rest/api/content/search", {
213
+ const response = await this.client.get("rest/api/content/search", {
193
214
  params: {
194
215
  cql,
195
216
  start,
@@ -237,20 +258,23 @@ var ConflictError = class extends TdeCollabError {
237
258
 
238
259
  // tools/common/http-client.ts
239
260
  function createHttpClient(config) {
261
+ const normalizedBaseUrl = config.baseUrl.endsWith("/") ? config.baseUrl : `${config.baseUrl}/`;
240
262
  const client = axios.create({
241
- baseURL: config.baseUrl,
263
+ baseURL: normalizedBaseUrl,
242
264
  timeout: 3e4,
243
265
  // 30초 타임아웃
266
+ adapter: "http",
267
+ // Electron 환경(Obsidian)에서 XHR 대신 Node.js http 모듈을 사용하여 CORS 우회
244
268
  headers: {
245
269
  "Content-Type": "application/json"
246
270
  }
247
271
  });
248
272
  client.interceptors.request.use((reqConfig) => {
249
- if (config.auth.token && !reqConfig.headers.Authorization && !reqConfig.headers["PRIVATE-TOKEN"]) {
250
- reqConfig.headers.Authorization = `Bearer ${config.auth.token}`;
251
- } else if (config.auth.username && config.auth.token && !reqConfig.headers.Authorization) {
273
+ if (config.auth.username && config.auth.token && !reqConfig.headers.Authorization) {
252
274
  const token = Buffer.from(`${config.auth.username}:${config.auth.token}`).toString("base64");
253
275
  reqConfig.headers.Authorization = `Basic ${token}`;
276
+ } else if (config.auth.token && !reqConfig.headers.Authorization && !reqConfig.headers["PRIVATE-TOKEN"]) {
277
+ reqConfig.headers.Authorization = `Bearer ${config.auth.token}`;
254
278
  }
255
279
  logger.debug(`[HTTP Request] ${reqConfig.method?.toUpperCase()} ${reqConfig.url}`, {
256
280
  headers: reqConfig.headers,
@@ -318,7 +342,8 @@ function getEnvOrThrow(key, description) {
318
342
  }
319
343
  function loadConfluenceConfig() {
320
344
  const baseUrl = getEnvOrThrow("CONFLUENCE_BASE_URL", "Confluence \uAE30\uBCF8 URL");
321
- const username = process.env.CONFLUENCE_USERNAME;
345
+ const authType = (process.env.CONFLUENCE_AUTH_TYPE || "bearer").toLowerCase();
346
+ const username = authType === "basic" ? process.env.CONFLUENCE_USERNAME : void 0;
322
347
  const token = getEnvOrThrow("CONFLUENCE_API_TOKEN", "Confluence PAT \uD1A0\uD070");
323
348
  const mermaidMacroName = process.env.CONFLUENCE_MERMAID_MACRO_NAME || "mermaiddiagram";
324
349
  const inlineCodeStyle = process.env.CONFLUENCE_INLINE_CODE_STYLE || "color: #d04437; font-weight: bold;";
@@ -365,14 +390,191 @@ function loadGitlabConfig() {
365
390
 
366
391
  // tools/confluence/converters/md-to-storage.ts
367
392
  import MarkdownIt from "markdown-it";
393
+ var DEFAULT_MERMAID_MACRO_NAME = "mermaiddiagram";
394
+ var DEFAULT_INLINE_CODE_STYLE = "color: #d04437; font-weight: bold;";
395
+ var TASK_ITEM_PATTERN = /^\[([ xX])\]\s+/;
396
+ function decodeLocalImagePath(value) {
397
+ try {
398
+ return decodeURI(value);
399
+ } catch {
400
+ return value;
401
+ }
402
+ }
403
+ function escapeXmlAttribute(value) {
404
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
405
+ }
406
+ function parseImageDimensionTitle(title) {
407
+ if (!title) {
408
+ return "";
409
+ }
410
+ const width = title.match(/\bwidth=(\d+)\b/i)?.[1];
411
+ const height = title.match(/\bheight=(\d+)\b/i)?.[1];
412
+ const attrs = [
413
+ width ? ` ac:width="${escapeXmlAttribute(width)}"` : "",
414
+ height ? ` ac:height="${escapeXmlAttribute(height)}"` : ""
415
+ ];
416
+ return attrs.join("");
417
+ }
418
+ function getTaskMarker(content) {
419
+ const match = content.match(TASK_ITEM_PATTERN);
420
+ if (!match) {
421
+ return null;
422
+ }
423
+ return {
424
+ checked: match[1].toLowerCase() === "x",
425
+ body: content.slice(match[0].length)
426
+ };
427
+ }
428
+ function findMatchingCloseIndex(tokens, openIndex) {
429
+ const openToken = tokens[openIndex];
430
+ let nesting = 1;
431
+ for (let idx = openIndex + 1; idx < tokens.length; idx++) {
432
+ const token = tokens[idx];
433
+ if (token.type === openToken.type) {
434
+ nesting++;
435
+ }
436
+ if (token.type === openToken.type.replace("_open", "_close")) {
437
+ nesting--;
438
+ if (nesting === 0) {
439
+ return idx;
440
+ }
441
+ }
442
+ }
443
+ return openIndex;
444
+ }
445
+ function annotateTaskListTokens(tokens) {
446
+ const listItemStack = [];
447
+ for (let idx = 0; idx < tokens.length; idx++) {
448
+ const token = tokens[idx];
449
+ if (token.type === "list_item_open") {
450
+ listItemStack.push(idx);
451
+ continue;
452
+ }
453
+ if (token.type === "list_item_close") {
454
+ listItemStack.pop();
455
+ continue;
456
+ }
457
+ if (token.type !== "inline" || listItemStack.length === 0) {
458
+ continue;
459
+ }
460
+ const task = getTaskMarker(token.content);
461
+ if (!task) {
462
+ continue;
463
+ }
464
+ const listItemOpenIndex = listItemStack[listItemStack.length - 1];
465
+ if (tokens[listItemOpenIndex].meta?.task) {
466
+ continue;
467
+ }
468
+ token.content = task.body;
469
+ if (token.children?.[0]?.type === "text") {
470
+ token.children[0].content = token.children[0].content.replace(TASK_ITEM_PATTERN, "");
471
+ }
472
+ tokens[listItemOpenIndex].meta = {
473
+ ...tokens[listItemOpenIndex].meta || {},
474
+ task
475
+ };
476
+ if (tokens[idx - 1]?.type === "paragraph_open") {
477
+ tokens[idx - 1].meta = {
478
+ ...tokens[idx - 1].meta || {},
479
+ taskBodyParagraph: true
480
+ };
481
+ }
482
+ if (tokens[idx + 1]?.type === "paragraph_close") {
483
+ tokens[idx + 1].meta = {
484
+ ...tokens[idx + 1].meta || {},
485
+ taskBodyParagraph: true
486
+ };
487
+ }
488
+ }
489
+ annotateTaskListBoundaries(tokens);
490
+ annotateTaskListCloseTokens(tokens);
491
+ }
492
+ function annotateTaskListBoundaries(tokens) {
493
+ for (let idx = 0; idx < tokens.length; idx++) {
494
+ const token = tokens[idx];
495
+ if (token.type !== "bullet_list_open") {
496
+ continue;
497
+ }
498
+ const closeIndex = findMatchingCloseIndex(tokens, idx);
499
+ const directItemIndexes = [];
500
+ for (let cursor = idx + 1; cursor < closeIndex; cursor++) {
501
+ if (tokens[cursor].type === "list_item_open" && tokens[cursor].level === token.level + 1) {
502
+ directItemIndexes.push(cursor);
503
+ }
504
+ }
505
+ if (directItemIndexes.length === 0) {
506
+ continue;
507
+ }
508
+ const itemKinds = directItemIndexes.map((itemIndex) => !!tokens[itemIndex].meta?.task);
509
+ const firstIsNormal = !itemKinds[0];
510
+ const lastIsNormal = !itemKinds[itemKinds.length - 1];
511
+ token.meta = { ...token.meta || {}, renderList: firstIsNormal };
512
+ tokens[closeIndex].meta = { ...tokens[closeIndex].meta || {}, renderList: lastIsNormal };
513
+ directItemIndexes.forEach((itemIndex, itemPosition) => {
514
+ const itemToken = tokens[itemIndex];
515
+ if (!itemToken.meta?.task) {
516
+ return;
517
+ }
518
+ itemToken.meta.taskCloseParentBefore = itemPosition > 0 && !itemKinds[itemPosition - 1];
519
+ itemToken.meta.taskOpenParentAfter = itemPosition < itemKinds.length - 1 && !itemKinds[itemPosition + 1];
520
+ });
521
+ }
522
+ }
523
+ function annotateTaskListCloseTokens(tokens) {
524
+ const listItemStack = [];
525
+ for (let idx = 0; idx < tokens.length; idx++) {
526
+ const token = tokens[idx];
527
+ if (token.type === "list_item_open") {
528
+ listItemStack.push(idx);
529
+ continue;
530
+ }
531
+ if (token.type !== "list_item_close") {
532
+ continue;
533
+ }
534
+ const openIndex = listItemStack.pop();
535
+ if (openIndex === void 0 || !tokens[openIndex].meta?.task) {
536
+ continue;
537
+ }
538
+ token.meta = {
539
+ ...token.meta || {},
540
+ task: tokens[openIndex].meta.task,
541
+ taskOpenParentAfter: tokens[openIndex].meta.taskOpenParentAfter
542
+ };
543
+ }
544
+ }
545
+ function stripMarkdownFrontmatter(markdown) {
546
+ const frontmatterMatch = markdown.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/);
547
+ if (!frontmatterMatch) {
548
+ return markdown;
549
+ }
550
+ return markdown.slice(frontmatterMatch[0].length).replace(/^\r?\n/, "");
551
+ }
552
+ function stripLeakedConfluencePageIdArtifacts(markdown) {
553
+ let prepared = markdown;
554
+ const leakedPageIdPattern = /^(?:[ \t]*\r?\n)*(?:---|\*\*\*)[ \t]*\r?\n+(?:[ \t]*\r?\n)*#{1,6}[ \t]+confluence\\?_page\\?_id:[ \t]*\d+[ \t]*\r?\n+/i;
555
+ while (leakedPageIdPattern.test(prepared)) {
556
+ prepared = prepared.replace(leakedPageIdPattern, "").replace(/^\r?\n/, "");
557
+ }
558
+ return prepared;
559
+ }
560
+ function prepareMarkdownForConfluenceStorage(markdown) {
561
+ const withoutLeadingArtifacts = stripLeakedConfluencePageIdArtifacts(markdown);
562
+ const withoutFrontmatter = stripMarkdownFrontmatter(withoutLeadingArtifacts);
563
+ return stripLeakedConfluencePageIdArtifacts(withoutFrontmatter);
564
+ }
368
565
  var MarkdownToStorageConverter = class {
369
566
  md;
370
567
  mermaidMacroName;
371
568
  inlineCodeStyle;
372
- constructor() {
373
- const config = loadConfluenceConfig();
374
- this.mermaidMacroName = config.mermaidMacroName;
375
- this.inlineCodeStyle = config.inlineCodeStyle;
569
+ constructor(options) {
570
+ if (options) {
571
+ this.mermaidMacroName = options.mermaidMacroName || DEFAULT_MERMAID_MACRO_NAME;
572
+ this.inlineCodeStyle = options.inlineCodeStyle || DEFAULT_INLINE_CODE_STYLE;
573
+ } else {
574
+ const config = loadConfluenceConfig();
575
+ this.mermaidMacroName = config.mermaidMacroName;
576
+ this.inlineCodeStyle = config.inlineCodeStyle;
577
+ }
376
578
  this.md = new MarkdownIt({
377
579
  html: true,
378
580
  linkify: true,
@@ -380,6 +582,50 @@ var MarkdownToStorageConverter = class {
380
582
  xhtmlOut: true
381
583
  // Confluence XML 파서와의 호환성을 위해 XHTML 출력 활성화
382
584
  });
585
+ this.md.core.ruler.after("inline", "confluence_task_list", (state) => {
586
+ annotateTaskListTokens(state.tokens);
587
+ });
588
+ this.md.renderer.rules.bullet_list_open = (tokens, idx, options2, env, self) => {
589
+ if (tokens[idx].meta?.renderList === false) {
590
+ return "";
591
+ }
592
+ return self.renderToken(tokens, idx, options2);
593
+ };
594
+ this.md.renderer.rules.bullet_list_close = (tokens, idx, options2, env, self) => {
595
+ if (tokens[idx].meta?.renderList === false) {
596
+ return "";
597
+ }
598
+ return self.renderToken(tokens, idx, options2);
599
+ };
600
+ this.md.renderer.rules.list_item_open = (tokens, idx, options2, env, self) => {
601
+ const task = tokens[idx].meta?.task;
602
+ if (!task) {
603
+ return self.renderToken(tokens, idx, options2);
604
+ }
605
+ const prefix = tokens[idx].meta.taskCloseParentBefore ? "</ul>\n" : "";
606
+ const status = task.checked ? "complete" : "incomplete";
607
+ return `${prefix}<ac:task-list><ac:task><ac:task-status>${status}</ac:task-status><ac:task-body>`;
608
+ };
609
+ this.md.renderer.rules.list_item_close = (tokens, idx, options2, env, self) => {
610
+ if (!tokens[idx].meta?.task) {
611
+ return self.renderToken(tokens, idx, options2);
612
+ }
613
+ const suffix = tokens[idx].meta.taskOpenParentAfter ? "\n<ul>" : "";
614
+ return suffix;
615
+ };
616
+ this.md.renderer.rules.paragraph_open = (tokens, idx, options2, env, self) => {
617
+ if (tokens[idx].meta?.taskBodyParagraph) {
618
+ return "";
619
+ }
620
+ return self.renderToken(tokens, idx, options2);
621
+ };
622
+ this.md.renderer.rules.paragraph_close = (tokens, idx, options2, env, self) => {
623
+ const task = tokens[idx].meta?.taskBodyParagraph;
624
+ if (task) {
625
+ return "</ac:task-body></ac:task></ac:task-list>";
626
+ }
627
+ return self.renderToken(tokens, idx, options2);
628
+ };
383
629
  this.md.renderer.rules.fence = (tokens, idx) => {
384
630
  const token = tokens[idx];
385
631
  const code = token.content.trim();
@@ -400,17 +646,19 @@ var MarkdownToStorageConverter = class {
400
646
  <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
401
647
  </ac:structured-macro>`;
402
648
  };
403
- this.md.renderer.rules.image = (tokens, idx, options, env, self) => {
649
+ this.md.renderer.rules.image = (tokens, idx, options2, env, self) => {
404
650
  const token = tokens[idx];
405
651
  const src = token.attrGet("src") || "";
406
652
  const alt = token.content || "";
653
+ const dimensionAttrs = parseImageDimensionTitle(token.attrGet("title"));
407
654
  const isExternal = src.startsWith("http://") || src.startsWith("https://");
408
- const altAttr = alt ? ` ac:alt="${this.md.utils.escapeHtml(alt)}"` : "";
655
+ const altAttr = alt ? ` ac:alt="${escapeXmlAttribute(alt)}"` : "";
409
656
  if (isExternal) {
410
- return `<ac:image${altAttr}><ri:url ri:value="${src}" /></ac:image>`;
657
+ return `<ac:image${altAttr}${dimensionAttrs}><ri:url ri:value="${escapeXmlAttribute(src)}" /></ac:image>`;
411
658
  } else {
412
- const filename = src.split("/").pop() || src;
413
- return `<ac:image${altAttr}><ri:attachment ri:filename="${filename}" /></ac:image>`;
659
+ const decodedSrc = decodeLocalImagePath(src);
660
+ const filename = decodedSrc.split("/").pop() || decodedSrc;
661
+ return `<ac:image${altAttr}${dimensionAttrs}><ri:attachment ri:filename="${escapeXmlAttribute(filename)}" /></ac:image>`;
414
662
  }
415
663
  };
416
664
  this.md.renderer.rules.code_inline = (tokens, idx) => {
@@ -420,10 +668,10 @@ var MarkdownToStorageConverter = class {
420
668
  };
421
669
  }
422
670
  convert(markdown) {
423
- return this.md.render(markdown);
671
+ return this.md.render(prepareMarkdownForConfluenceStorage(markdown));
424
672
  }
425
673
  extractLocalImages(markdown) {
426
- const tokens = this.md.parse(markdown, {});
674
+ const tokens = this.md.parse(prepareMarkdownForConfluenceStorage(markdown), {});
427
675
  const localImages = /* @__PURE__ */ new Set();
428
676
  const walk = (tokens2) => {
429
677
  for (const token of tokens2) {
@@ -446,7 +694,38 @@ var MarkdownToStorageConverter = class {
446
694
  // tools/confluence/converters/storage-to-md.ts
447
695
  import TurndownService from "turndown";
448
696
  import { gfm } from "turndown-plugin-gfm";
449
- import { JSDOM } from "jsdom";
697
+ import { createRequire } from "module";
698
+ function parseHtmlDocument(html) {
699
+ if (typeof window !== "undefined" && typeof window.DOMParser !== "undefined") {
700
+ const parser = new window.DOMParser();
701
+ return parser.parseFromString(html, "text/html");
702
+ }
703
+ const requireBase = process.argv[1] || `${process.cwd()}/package.json`;
704
+ const nodeRequire = createRequire(requireBase);
705
+ const { JSDOM } = nodeRequire("jsdom");
706
+ return new JSDOM(html).window.document;
707
+ }
708
+ function getXmlAttribute(attrs, name) {
709
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
710
+ return attrs.match(new RegExp(`(?:^|\\s)${escapedName}="([^"]*)"`, "i"))?.[1];
711
+ }
712
+ function escapeHtmlAttribute(value) {
713
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
714
+ }
715
+ function escapeMarkdownImageText(value) {
716
+ return value.replace(/\\/g, "\\\\").replace(/]/g, "\\]");
717
+ }
718
+ function escapeMarkdownImageDestination(value) {
719
+ return value.replace(/\)/g, "\\)");
720
+ }
721
+ function buildImageDimensionAttributes(attrs) {
722
+ const width = getXmlAttribute(attrs, "ac:width") || getXmlAttribute(attrs, "width");
723
+ const height = getXmlAttribute(attrs, "ac:height") || getXmlAttribute(attrs, "height");
724
+ return [
725
+ width ? ` width="${escapeHtmlAttribute(width)}"` : "",
726
+ height ? ` height="${escapeHtmlAttribute(height)}"` : ""
727
+ ].join("");
728
+ }
450
729
  var StorageToMarkdownConverter = class {
451
730
  turndown;
452
731
  jiraBaseUrl;
@@ -464,6 +743,37 @@ var StorageToMarkdownConverter = class {
464
743
  this.setupRules();
465
744
  }
466
745
  setupRules() {
746
+ this.turndown.addRule("imagesWithDimensions", {
747
+ filter: (node) => {
748
+ if (node.nodeName.toLowerCase() !== "img") return false;
749
+ const element = node;
750
+ return element.hasAttribute("width") || element.hasAttribute("height");
751
+ },
752
+ replacement: (_content, node) => {
753
+ const element = node;
754
+ const src = element.getAttribute("src") || "";
755
+ const alt = element.getAttribute("alt") || "";
756
+ const width = element.getAttribute("width");
757
+ const height = element.getAttribute("height");
758
+ const title = [
759
+ width ? `width=${width}` : "",
760
+ height ? `height=${height}` : ""
761
+ ].filter(Boolean).join(" ");
762
+ return `![${escapeMarkdownImageText(alt)}](${escapeMarkdownImageDestination(src)} "${title}")`;
763
+ }
764
+ });
765
+ this.turndown.addRule("lists", {
766
+ filter: (node) => {
767
+ const nodeName = node.nodeName.toLowerCase();
768
+ if (nodeName !== "ul" && nodeName !== "ol") return false;
769
+ return Array.from(node.children).some((child) => {
770
+ return child.nodeName.toLowerCase() === "li" && this.getExplicitListItemDepth(child) !== void 0;
771
+ });
772
+ },
773
+ replacement: (_content, node) => {
774
+ return this.renderList(node, 0);
775
+ }
776
+ });
467
777
  this.turndown.addRule("tables", {
468
778
  filter: ["table"],
469
779
  replacement: (content, node) => {
@@ -563,6 +873,83 @@ ${bodyMd}
563
873
  }
564
874
  });
565
875
  }
876
+ renderList(listElement, depth) {
877
+ const isOrdered = listElement.nodeName.toLowerCase() === "ol";
878
+ const start = Number(listElement.getAttribute("start") || "1");
879
+ let orderedIndex = Number.isFinite(start) && start > 0 ? start : 1;
880
+ const renderedItems = Array.from(listElement.children).filter((child) => child.nodeName.toLowerCase() === "li").map((item) => {
881
+ const itemDepth = this.getListItemDepth(item, depth);
882
+ const marker = isOrdered ? `${orderedIndex++}.` : "-";
883
+ const indent = " ".repeat(itemDepth);
884
+ const content = this.renderListItemContent(item);
885
+ const firstLine = `${indent}${marker}${content ? ` ${content.split("\n")[0]}` : ""}`;
886
+ const remainingLines = content.split("\n").slice(1).filter((line) => line.trim().length > 0).map((line) => `${indent} ${line}`).join("\n");
887
+ const nestedLists = Array.from(item.children).filter((child) => {
888
+ const nodeName = child.nodeName.toLowerCase();
889
+ return nodeName === "ul" || nodeName === "ol";
890
+ }).map((child) => this.renderList(child, itemDepth + 1).trim()).filter(Boolean).join("\n");
891
+ return [firstLine, remainingLines, nestedLists].filter(Boolean).join("\n");
892
+ }).filter(Boolean);
893
+ return `
894
+
895
+ ${renderedItems.join("\n")}
896
+
897
+ `;
898
+ }
899
+ renderListItemContent(item) {
900
+ const clone = item.cloneNode(true);
901
+ clone.querySelectorAll("ul, ol").forEach((child) => child.remove());
902
+ return this.turndown.turndown(clone.innerHTML).replace(/\n{2,}/g, "\n").trim();
903
+ }
904
+ getListItemDepth(item, fallbackDepth) {
905
+ return this.getExplicitListItemDepth(item) ?? fallbackDepth;
906
+ }
907
+ getExplicitListItemDepth(item) {
908
+ const explicitDepth = item.getAttribute("data-indent-level") || item.getAttribute("data-indent") || item.getAttribute("data-level");
909
+ if (explicitDepth !== null) {
910
+ const parsedDepth = Number(explicitDepth);
911
+ if (Number.isFinite(parsedDepth) && parsedDepth >= 0) {
912
+ return parsedDepth;
913
+ }
914
+ }
915
+ const className = item.getAttribute("class") || "";
916
+ const classDepth = className.match(/(?:^|\s)(?:ql-indent|indent)-(\d+)(?:\s|$)/)?.[1];
917
+ if (classDepth) {
918
+ const parsedDepth = Number(classDepth);
919
+ if (Number.isFinite(parsedDepth) && parsedDepth >= 0) {
920
+ return parsedDepth;
921
+ }
922
+ }
923
+ const style = item.getAttribute("style") || "";
924
+ const marginLeftPx = style.match(/margin-left:\s*(\d+(?:\.\d+)?)px/i)?.[1];
925
+ if (marginLeftPx) {
926
+ const parsedPx = Number(marginLeftPx);
927
+ if (Number.isFinite(parsedPx) && parsedPx > 0) {
928
+ return Math.max(0, Math.round(parsedPx / 40));
929
+ }
930
+ }
931
+ return void 0;
932
+ }
933
+ normalizeMalformedConfluenceLists(document) {
934
+ let changed = true;
935
+ while (changed) {
936
+ changed = false;
937
+ const lists = Array.from(document.querySelectorAll("ul, ol"));
938
+ for (const list of lists) {
939
+ const childLists = Array.from(list.children).filter((child) => {
940
+ const nodeName = child.nodeName.toLowerCase();
941
+ return nodeName === "ul" || nodeName === "ol";
942
+ });
943
+ for (const childList of childLists) {
944
+ const previous = childList.previousElementSibling;
945
+ if (previous?.nodeName.toLowerCase() === "li") {
946
+ previous.appendChild(childList);
947
+ changed = true;
948
+ }
949
+ }
950
+ }
951
+ }
952
+ }
566
953
  convert(storageHtml, imageUrlMap, jiraIssueMap) {
567
954
  this.jiraIssueMap = jiraIssueMap;
568
955
  if (!storageHtml) return "";
@@ -571,14 +958,16 @@ ${bodyMd}
571
958
  }).replace(/<ac:structured-macro\s+ac:name="([^"]*)"/gi, '<div data-macro-name-tag data-macro-name="$1"').replace(/<\/ac:structured-macro>/gi, "</div>").replace(/<ac:parameter\s+ac:name="([^"]*)"/gi, '<div data-macro-param-tag data-macro-param-name="$1"').replace(/<\/ac:parameter>/gi, "</div>").replace(/<ac:plain-text-body>/gi, "<pre data-macro-body>").replace(/<\/ac:plain-text-body>/gi, "</pre>").replace(/<ac:rich-text-body>/gi, "<div data-macro-rich-body>").replace(/<\/ac:rich-text-body>/gi, "</div>").replace(/<ac:image([^>]*)>[\s\S]*?<ri:attachment\s+ri:filename="([^"]*)"\s*\/?>[\s\S]*?<\/ac:image>/gi, (match, attrs, filename) => {
572
959
  const altMatch = attrs.match(/ac:alt="([^"]*)"/i);
573
960
  const alt = altMatch ? altMatch[1] : filename;
574
- return `<img src="${filename}" alt="${alt}" />`;
961
+ const dimensions = buildImageDimensionAttributes(attrs);
962
+ return `<img src="${escapeHtmlAttribute(filename)}" alt="${escapeHtmlAttribute(alt)}"${dimensions} />`;
575
963
  }).replace(/<ac:image([^>]*)>[\s\S]*?<ri:url\s+ri:value="([^"]*)"\s*\/?>[\s\S]*?<\/ac:image>/gi, (match, attrs, url) => {
576
964
  const altMatch = attrs.match(/ac:alt="([^"]*)"/i);
577
965
  const alt = altMatch ? altMatch[1] : "";
578
- return `<img src="${url}" alt="${alt}" />`;
966
+ const dimensions = buildImageDimensionAttributes(attrs);
967
+ return `<img src="${escapeHtmlAttribute(url)}" alt="${escapeHtmlAttribute(alt)}"${dimensions} />`;
579
968
  });
580
- const dom = new JSDOM(processedHtml);
581
- const document = dom.window.document;
969
+ const document = parseHtmlDocument(processedHtml);
970
+ this.normalizeMalformedConfluenceLists(document);
582
971
  if (imageUrlMap && imageUrlMap.size > 0) {
583
972
  const images = document.querySelectorAll("img");
584
973
  images.forEach((img) => {
@@ -630,7 +1019,7 @@ var JiraIssueApi = class {
630
1019
  if (params.customFields) {
631
1020
  Object.assign(fields, params.customFields);
632
1021
  }
633
- const response = await this.client.post("/rest/api/2/issue", { fields });
1022
+ const response = await this.client.post("rest/api/2/issue", { fields });
634
1023
  return response.data;
635
1024
  }
636
1025
  async updateIssue(issueKey, params) {
@@ -716,7 +1105,7 @@ var JiraSearchApi = class {
716
1105
  if (fields && fields.length > 0) {
717
1106
  params.fields = fields.join(",");
718
1107
  }
719
- const response = await this.client.get("/rest/api/2/search", { params });
1108
+ const response = await this.client.get("rest/api/2/search", { params });
720
1109
  return response.data;
721
1110
  }
722
1111
  };
@@ -829,6 +1218,7 @@ function createGitlabClient(config) {
829
1218
 
830
1219
  export {
831
1220
  loadEnv,
1221
+ getEnvSource,
832
1222
  ConfluenceContentApi,
833
1223
  ConfluenceSpaceApi,
834
1224
  ConfluenceSearchApi,
@@ -847,4 +1237,4 @@ export {
847
1237
  GitlabPipelineApi,
848
1238
  createGitlabClient
849
1239
  };
850
- //# sourceMappingURL=chunk-5OB3KU5D.js.map
1240
+ //# sourceMappingURL=chunk-SIKUIQKX.js.map