markdown-to-confluence 0.2.4__tar.gz → 0.2.6__tar.gz

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.
Files changed (33) hide show
  1. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/PKG-INFO +23 -15
  2. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/README.md +18 -10
  3. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/PKG-INFO +23 -15
  4. markdown_to_confluence-0.2.6/markdown_to_confluence.egg-info/requires.txt +9 -0
  5. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/__init__.py +1 -1
  6. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/__main__.py +11 -0
  7. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/api.py +37 -11
  8. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/application.py +53 -20
  9. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/converter.py +50 -27
  10. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/emoji.py +8 -0
  11. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/matcher.py +8 -0
  12. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/mermaid.py +13 -1
  13. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/processor.py +44 -13
  14. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/properties.py +8 -0
  15. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/util.py +8 -0
  16. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/setup.cfg +4 -4
  17. markdown_to_confluence-0.2.6/setup.py +12 -0
  18. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/tests/test_conversion.py +27 -3
  19. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/tests/test_matcher.py +9 -0
  20. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/tests/test_mermaid.py +8 -1
  21. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/tests/test_processor.py +8 -2
  22. markdown_to_confluence-0.2.4/markdown_to_confluence.egg-info/requires.txt +0 -9
  23. markdown_to_confluence-0.2.4/setup.py +0 -4
  24. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/LICENSE +0 -0
  25. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/SOURCES.txt +0 -0
  26. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
  27. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
  28. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/top_level.txt +0 -0
  29. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/markdown_to_confluence.egg-info/zip-safe +0 -0
  30. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/entities.dtd +0 -0
  31. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/puppeteer-config.json +0 -0
  32. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/md2conf/py.typed +0 -0
  33. {markdown_to_confluence-0.2.4 → markdown_to_confluence-0.2.6}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-to-confluence
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -22,10 +22,10 @@ Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: lxml>=5.3
25
- Requires-Dist: types-lxml>=2024.8.7
26
- Requires-Dist: markdown>=3.6
27
- Requires-Dist: types-markdown>=3.6
28
- Requires-Dist: pymdown-extensions>=10.9
25
+ Requires-Dist: types-lxml>=2024.11.8
26
+ Requires-Dist: markdown>=3.7
27
+ Requires-Dist: types-markdown>=3.7
28
+ Requires-Dist: pymdown-extensions>=10.12
29
29
  Requires-Dist: pyyaml>=6.0
30
30
  Requires-Dist: types-PyYAML>=6.0
31
31
  Requires-Dist: requests>=2.32
@@ -75,11 +75,11 @@ npm install -g @mermaid-js/mermaid-cli
75
75
 
76
76
  In order to get started, you will need
77
77
 
78
- * your organization domain name (e.g. `instructure.atlassian.net`),
78
+ * your organization domain name (e.g. `example.atlassian.net`),
79
79
  * base path for Confluence wiki (typically `/wiki/` for managed Confluence, `/` for on-premise)
80
80
  * your Confluence username (e.g. `levente.hunyadi@instructure.com`) (only if required by your deployment),
81
81
  * a Confluence API token (a string of alphanumeric characters), and
82
- * the space key in Confluence (e.g. `DAP`) you are publishing content to.
82
+ * the space key in Confluence (e.g. `SPACE`) you are publishing content to.
83
83
 
84
84
  ### Obtaining an API token
85
85
 
@@ -93,11 +93,11 @@ In order to get started, you will need
93
93
  Confluence organization domain, base path, username, API token and space key can be specified at runtime or set as Confluence environment variables (e.g. add to your `~/.profile` on Linux, or `~/.bash_profile` or `~/.zshenv` on MacOS):
94
94
 
95
95
  ```bash
96
- export CONFLUENCE_DOMAIN='instructure.atlassian.net'
96
+ export CONFLUENCE_DOMAIN='example.atlassian.net'
97
97
  export CONFLUENCE_PATH='/wiki/'
98
98
  export CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
99
99
  export CONFLUENCE_API_KEY='0123456789abcdef'
100
- export CONFLUENCE_SPACE_KEY='DAP'
100
+ export CONFLUENCE_SPACE_KEY='SPACE'
101
101
  ```
102
102
 
103
103
  On Windows, these can be set via system properties.
@@ -129,7 +129,7 @@ The above tells the tool to synchronize the Markdown file with the given Conflue
129
129
  If you work in an environment where there are multiple Confluence spaces, and some Markdown pages may go into one space, whereas other pages may go into another, you can set the target space on a per-document basis:
130
130
 
131
131
  ```markdown
132
- <!-- confluence-space-key: DAP -->
132
+ <!-- confluence-space-key: SPACE -->
133
133
  ```
134
134
 
135
135
  This overrides the default space set via command-line arguments or environment variables.
@@ -146,9 +146,17 @@ Provide generated-by prompt text in the Markdown file with a tag:
146
146
 
147
147
  Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
148
148
 
149
+ ### Publishing a single page
150
+
151
+ *md2conf* has two modes of operation: *single-page mode* and *directory mode*.
152
+
153
+ In single-page mode, you specify a single Markdown file as the source, which can contain absolute links to external locations (e.g. `https://example.com`) but not relative links to other pages (e.g. `local.md`). In other words, the page must be stand-alone.
154
+
149
155
  ### Publishing a directory
150
156
 
151
- *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file if you pass a directory as `mdpath`. This will traverse the specified directory recursively, and synchronize each Markdown file.
157
+ *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file in *directory mode* if you pass a directory as the source. This will traverse the specified directory recursively, and synchronize each Markdown file.
158
+
159
+ First, *md2conf* builds an index of pages in the directory hierarchy. The index maps each Markdown file path to a Confluence page ID. Whenever a relative link is encountered in a Markdown file, the relative link is replaced with a Confluence URL to the referenced page with the help of the index. All relative links must point to Markdown files that are located in the directory hierarchy.
152
160
 
153
161
  If a Markdown file doesn't yet pair up with a Confluence page, *md2conf* creates a new page and assigns a parent. Parent-child relationships are reflected in the navigation panel in Confluence. You can set a root page ID with the command-line option `-r`, which constitutes the topmost parent. (This could correspond to the landing page of your Confluence space. The Confluence page ID is always revealed when you edit a page.) Whenever a directory contains the file `index.md` or `README.md`, this page becomes the future parent page, and all Markdown files in this directory (and possibly nested directories) become its child pages (unless they already have a page ID). However, if an `index.md` or `README.md` file is subsequently found in one of the nested directories, it becomes the parent page of that directory, and any of its subdirectories.
154
162
 
@@ -216,7 +224,7 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
216
224
  With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
217
225
 
218
226
  ```sh
219
- docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
227
+ docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d example.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s SPACE ./
220
228
  ```
221
229
 
222
230
  Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
@@ -234,11 +242,11 @@ With the `Dockerfile` approach, you can extend the base image:
234
242
  ```Dockerfile
235
243
  FROM leventehunyadi/md2conf:latest
236
244
 
237
- ENV CONFLUENCE_DOMAIN='instructure.atlassian.net'
245
+ ENV CONFLUENCE_DOMAIN='example.atlassian.net'
238
246
  ENV CONFLUENCE_PATH='/wiki/'
239
247
  ENV CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
240
248
  ENV CONFLUENCE_API_KEY='0123456789abcdef'
241
- ENV CONFLUENCE_SPACE_KEY='DAP'
249
+ ENV CONFLUENCE_SPACE_KEY='SPACE'
242
250
 
243
251
  CMD ["./"]
244
252
  ```
@@ -248,5 +256,5 @@ Alternatively,
248
256
  ```Dockerfile
249
257
  FROM leventehunyadi/md2conf:latest
250
258
 
251
- CMD ["-d", "instructure.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "DAP", "./"]
259
+ CMD ["-d", "example.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "SPACE", "./"]
252
260
  ```
@@ -42,11 +42,11 @@ npm install -g @mermaid-js/mermaid-cli
42
42
 
43
43
  In order to get started, you will need
44
44
 
45
- * your organization domain name (e.g. `instructure.atlassian.net`),
45
+ * your organization domain name (e.g. `example.atlassian.net`),
46
46
  * base path for Confluence wiki (typically `/wiki/` for managed Confluence, `/` for on-premise)
47
47
  * your Confluence username (e.g. `levente.hunyadi@instructure.com`) (only if required by your deployment),
48
48
  * a Confluence API token (a string of alphanumeric characters), and
49
- * the space key in Confluence (e.g. `DAP`) you are publishing content to.
49
+ * the space key in Confluence (e.g. `SPACE`) you are publishing content to.
50
50
 
51
51
  ### Obtaining an API token
52
52
 
@@ -60,11 +60,11 @@ In order to get started, you will need
60
60
  Confluence organization domain, base path, username, API token and space key can be specified at runtime or set as Confluence environment variables (e.g. add to your `~/.profile` on Linux, or `~/.bash_profile` or `~/.zshenv` on MacOS):
61
61
 
62
62
  ```bash
63
- export CONFLUENCE_DOMAIN='instructure.atlassian.net'
63
+ export CONFLUENCE_DOMAIN='example.atlassian.net'
64
64
  export CONFLUENCE_PATH='/wiki/'
65
65
  export CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
66
66
  export CONFLUENCE_API_KEY='0123456789abcdef'
67
- export CONFLUENCE_SPACE_KEY='DAP'
67
+ export CONFLUENCE_SPACE_KEY='SPACE'
68
68
  ```
69
69
 
70
70
  On Windows, these can be set via system properties.
@@ -96,7 +96,7 @@ The above tells the tool to synchronize the Markdown file with the given Conflue
96
96
  If you work in an environment where there are multiple Confluence spaces, and some Markdown pages may go into one space, whereas other pages may go into another, you can set the target space on a per-document basis:
97
97
 
98
98
  ```markdown
99
- <!-- confluence-space-key: DAP -->
99
+ <!-- confluence-space-key: SPACE -->
100
100
  ```
101
101
 
102
102
  This overrides the default space set via command-line arguments or environment variables.
@@ -113,9 +113,17 @@ Provide generated-by prompt text in the Markdown file with a tag:
113
113
 
114
114
  Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
115
115
 
116
+ ### Publishing a single page
117
+
118
+ *md2conf* has two modes of operation: *single-page mode* and *directory mode*.
119
+
120
+ In single-page mode, you specify a single Markdown file as the source, which can contain absolute links to external locations (e.g. `https://example.com`) but not relative links to other pages (e.g. `local.md`). In other words, the page must be stand-alone.
121
+
116
122
  ### Publishing a directory
117
123
 
118
- *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file if you pass a directory as `mdpath`. This will traverse the specified directory recursively, and synchronize each Markdown file.
124
+ *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file in *directory mode* if you pass a directory as the source. This will traverse the specified directory recursively, and synchronize each Markdown file.
125
+
126
+ First, *md2conf* builds an index of pages in the directory hierarchy. The index maps each Markdown file path to a Confluence page ID. Whenever a relative link is encountered in a Markdown file, the relative link is replaced with a Confluence URL to the referenced page with the help of the index. All relative links must point to Markdown files that are located in the directory hierarchy.
119
127
 
120
128
  If a Markdown file doesn't yet pair up with a Confluence page, *md2conf* creates a new page and assigns a parent. Parent-child relationships are reflected in the navigation panel in Confluence. You can set a root page ID with the command-line option `-r`, which constitutes the topmost parent. (This could correspond to the landing page of your Confluence space. The Confluence page ID is always revealed when you edit a page.) Whenever a directory contains the file `index.md` or `README.md`, this page becomes the future parent page, and all Markdown files in this directory (and possibly nested directories) become its child pages (unless they already have a page ID). However, if an `index.md` or `README.md` file is subsequently found in one of the nested directories, it becomes the parent page of that directory, and any of its subdirectories.
121
129
 
@@ -183,7 +191,7 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
183
191
  With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
184
192
 
185
193
  ```sh
186
- docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
194
+ docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d example.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s SPACE ./
187
195
  ```
188
196
 
189
197
  Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
@@ -201,11 +209,11 @@ With the `Dockerfile` approach, you can extend the base image:
201
209
  ```Dockerfile
202
210
  FROM leventehunyadi/md2conf:latest
203
211
 
204
- ENV CONFLUENCE_DOMAIN='instructure.atlassian.net'
212
+ ENV CONFLUENCE_DOMAIN='example.atlassian.net'
205
213
  ENV CONFLUENCE_PATH='/wiki/'
206
214
  ENV CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
207
215
  ENV CONFLUENCE_API_KEY='0123456789abcdef'
208
- ENV CONFLUENCE_SPACE_KEY='DAP'
216
+ ENV CONFLUENCE_SPACE_KEY='SPACE'
209
217
 
210
218
  CMD ["./"]
211
219
  ```
@@ -215,5 +223,5 @@ Alternatively,
215
223
  ```Dockerfile
216
224
  FROM leventehunyadi/md2conf:latest
217
225
 
218
- CMD ["-d", "instructure.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "DAP", "./"]
226
+ CMD ["-d", "example.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "SPACE", "./"]
219
227
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-to-confluence
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -22,10 +22,10 @@ Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: lxml>=5.3
25
- Requires-Dist: types-lxml>=2024.8.7
26
- Requires-Dist: markdown>=3.6
27
- Requires-Dist: types-markdown>=3.6
28
- Requires-Dist: pymdown-extensions>=10.9
25
+ Requires-Dist: types-lxml>=2024.11.8
26
+ Requires-Dist: markdown>=3.7
27
+ Requires-Dist: types-markdown>=3.7
28
+ Requires-Dist: pymdown-extensions>=10.12
29
29
  Requires-Dist: pyyaml>=6.0
30
30
  Requires-Dist: types-PyYAML>=6.0
31
31
  Requires-Dist: requests>=2.32
@@ -75,11 +75,11 @@ npm install -g @mermaid-js/mermaid-cli
75
75
 
76
76
  In order to get started, you will need
77
77
 
78
- * your organization domain name (e.g. `instructure.atlassian.net`),
78
+ * your organization domain name (e.g. `example.atlassian.net`),
79
79
  * base path for Confluence wiki (typically `/wiki/` for managed Confluence, `/` for on-premise)
80
80
  * your Confluence username (e.g. `levente.hunyadi@instructure.com`) (only if required by your deployment),
81
81
  * a Confluence API token (a string of alphanumeric characters), and
82
- * the space key in Confluence (e.g. `DAP`) you are publishing content to.
82
+ * the space key in Confluence (e.g. `SPACE`) you are publishing content to.
83
83
 
84
84
  ### Obtaining an API token
85
85
 
@@ -93,11 +93,11 @@ In order to get started, you will need
93
93
  Confluence organization domain, base path, username, API token and space key can be specified at runtime or set as Confluence environment variables (e.g. add to your `~/.profile` on Linux, or `~/.bash_profile` or `~/.zshenv` on MacOS):
94
94
 
95
95
  ```bash
96
- export CONFLUENCE_DOMAIN='instructure.atlassian.net'
96
+ export CONFLUENCE_DOMAIN='example.atlassian.net'
97
97
  export CONFLUENCE_PATH='/wiki/'
98
98
  export CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
99
99
  export CONFLUENCE_API_KEY='0123456789abcdef'
100
- export CONFLUENCE_SPACE_KEY='DAP'
100
+ export CONFLUENCE_SPACE_KEY='SPACE'
101
101
  ```
102
102
 
103
103
  On Windows, these can be set via system properties.
@@ -129,7 +129,7 @@ The above tells the tool to synchronize the Markdown file with the given Conflue
129
129
  If you work in an environment where there are multiple Confluence spaces, and some Markdown pages may go into one space, whereas other pages may go into another, you can set the target space on a per-document basis:
130
130
 
131
131
  ```markdown
132
- <!-- confluence-space-key: DAP -->
132
+ <!-- confluence-space-key: SPACE -->
133
133
  ```
134
134
 
135
135
  This overrides the default space set via command-line arguments or environment variables.
@@ -146,9 +146,17 @@ Provide generated-by prompt text in the Markdown file with a tag:
146
146
 
147
147
  Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
148
148
 
149
+ ### Publishing a single page
150
+
151
+ *md2conf* has two modes of operation: *single-page mode* and *directory mode*.
152
+
153
+ In single-page mode, you specify a single Markdown file as the source, which can contain absolute links to external locations (e.g. `https://example.com`) but not relative links to other pages (e.g. `local.md`). In other words, the page must be stand-alone.
154
+
149
155
  ### Publishing a directory
150
156
 
151
- *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file if you pass a directory as `mdpath`. This will traverse the specified directory recursively, and synchronize each Markdown file.
157
+ *md2conf* allows you to convert and publish a directory of Markdown files rather than a single Markdown file in *directory mode* if you pass a directory as the source. This will traverse the specified directory recursively, and synchronize each Markdown file.
158
+
159
+ First, *md2conf* builds an index of pages in the directory hierarchy. The index maps each Markdown file path to a Confluence page ID. Whenever a relative link is encountered in a Markdown file, the relative link is replaced with a Confluence URL to the referenced page with the help of the index. All relative links must point to Markdown files that are located in the directory hierarchy.
152
160
 
153
161
  If a Markdown file doesn't yet pair up with a Confluence page, *md2conf* creates a new page and assigns a parent. Parent-child relationships are reflected in the navigation panel in Confluence. You can set a root page ID with the command-line option `-r`, which constitutes the topmost parent. (This could correspond to the landing page of your Confluence space. The Confluence page ID is always revealed when you edit a page.) Whenever a directory contains the file `index.md` or `README.md`, this page becomes the future parent page, and all Markdown files in this directory (and possibly nested directories) become its child pages (unless they already have a page ID). However, if an `index.md` or `README.md` file is subsequently found in one of the nested directories, it becomes the parent page of that directory, and any of its subdirectories.
154
162
 
@@ -216,7 +224,7 @@ You can run the Docker container via `docker run` or via `Dockerfile`. Either ca
216
224
  With `docker run`, you can pass Confluence domain, user, API and space key directly to `docker run`:
217
225
 
218
226
  ```sh
219
- docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d instructure.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s DAP ./
227
+ docker run --rm --name md2conf -v $(pwd):/data leventehunyadi/md2conf:latest -d example.atlassian.net -u levente.hunyadi@instructure.com -a 0123456789abcdef -s SPACE ./
220
228
  ```
221
229
 
222
230
  Alternatively, you can use a separate file `.env` to pass these parameters as environment variables:
@@ -234,11 +242,11 @@ With the `Dockerfile` approach, you can extend the base image:
234
242
  ```Dockerfile
235
243
  FROM leventehunyadi/md2conf:latest
236
244
 
237
- ENV CONFLUENCE_DOMAIN='instructure.atlassian.net'
245
+ ENV CONFLUENCE_DOMAIN='example.atlassian.net'
238
246
  ENV CONFLUENCE_PATH='/wiki/'
239
247
  ENV CONFLUENCE_USER_NAME='levente.hunyadi@instructure.com'
240
248
  ENV CONFLUENCE_API_KEY='0123456789abcdef'
241
- ENV CONFLUENCE_SPACE_KEY='DAP'
249
+ ENV CONFLUENCE_SPACE_KEY='SPACE'
242
250
 
243
251
  CMD ["./"]
244
252
  ```
@@ -248,5 +256,5 @@ Alternatively,
248
256
  ```Dockerfile
249
257
  FROM leventehunyadi/md2conf:latest
250
258
 
251
- CMD ["-d", "instructure.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "DAP", "./"]
259
+ CMD ["-d", "example.atlassian.net", "-u", "levente.hunyadi@instructure.com", "-a", "0123456789abcdef", "-s", "SPACE", "./"]
252
260
  ```
@@ -0,0 +1,9 @@
1
+ lxml>=5.3
2
+ types-lxml>=2024.11.8
3
+ markdown>=3.7
4
+ types-markdown>=3.7
5
+ pymdown-extensions>=10.12
6
+ pyyaml>=6.0
7
+ types-PyYAML>=6.0
8
+ requests>=2.32
9
+ types-requests>=2.32
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
5
5
  Confluence API endpoints to upload images and content.
6
6
  """
7
7
 
8
- __version__ = "0.2.4"
8
+ __version__ = "0.2.6"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2024, Levente Hunyadi"
11
11
  __license__ = "MIT"
@@ -1,3 +1,14 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Parses Markdown files, converts Markdown content into the Confluence Storage Format (XHTML), and invokes
5
+ Confluence API endpoints to upload images and content.
6
+
7
+ Copyright 2022-2024, Levente Hunyadi
8
+
9
+ :see: https://github.com/hunyadi/md2conf
10
+ """
11
+
1
12
  import argparse
2
13
  import logging
3
14
  import os.path
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import io
2
10
  import json
3
11
  import logging
@@ -170,17 +178,30 @@ class ConfluenceSession:
170
178
  def upload_attachment(
171
179
  self,
172
180
  page_id: str,
173
- attachment_path: Path,
174
181
  attachment_name: str,
182
+ *,
183
+ attachment_path: Optional[Path] = None,
175
184
  raw_data: Optional[bytes] = None,
185
+ content_type: Optional[str] = None,
176
186
  comment: Optional[str] = None,
177
- *,
178
187
  space_key: Optional[str] = None,
179
188
  force: bool = False,
180
189
  ) -> None:
181
- content_type = mimetypes.guess_type(attachment_path, strict=True)[0]
182
190
 
183
- if not raw_data and not attachment_path.is_file():
191
+ if attachment_path is None and raw_data is None:
192
+ raise ConfluenceError("required: `attachment_path` or `raw_data`")
193
+
194
+ if attachment_path is not None and raw_data is not None:
195
+ raise ConfluenceError("expected: either `attachment_path` or `raw_data`")
196
+
197
+ if content_type is None:
198
+ if attachment_path is not None:
199
+ name = str(attachment_path)
200
+ else:
201
+ name = attachment_name
202
+ content_type, _ = mimetypes.guess_type(name, strict=True)
203
+
204
+ if attachment_path is not None and not attachment_path.is_file():
184
205
  raise ConfluenceError(f"file not found: {attachment_path}")
185
206
 
186
207
  try:
@@ -188,14 +209,16 @@ class ConfluenceSession:
188
209
  page_id, attachment_name, space_key=space_key
189
210
  )
190
211
 
191
- if not raw_data:
212
+ if attachment_path is not None:
192
213
  if not force and attachment.file_size == attachment_path.stat().st_size:
193
214
  LOGGER.info("Up-to-date attachment: %s", attachment_name)
194
215
  return
195
- else:
216
+ elif raw_data is not None:
196
217
  if not force and attachment.file_size == len(raw_data):
197
218
  LOGGER.info("Up-to-date embedded image: %s", attachment_name)
198
219
  return
220
+ else:
221
+ raise NotImplementedError("never occurs")
199
222
 
200
223
  id = removeprefix(attachment.id, "att")
201
224
  path = f"/content/{page_id}/child/attachment/{id}/data"
@@ -205,7 +228,7 @@ class ConfluenceSession:
205
228
 
206
229
  url = self._build_url(path)
207
230
 
208
- if not raw_data:
231
+ if attachment_path is not None:
209
232
  with open(attachment_path, "rb") as attachment_file:
210
233
  file_to_upload = {
211
234
  "comment": comment,
@@ -222,24 +245,27 @@ class ConfluenceSession:
222
245
  files=file_to_upload, # type: ignore
223
246
  headers={"X-Atlassian-Token": "no-check"},
224
247
  )
225
- else:
248
+ elif raw_data is not None:
226
249
  LOGGER.info("Uploading raw data: %s", attachment_name)
227
250
 
251
+ raw_file = io.BytesIO(raw_data)
252
+ raw_file.name = attachment_name
228
253
  file_to_upload = {
229
254
  "comment": comment,
230
255
  "file": (
231
256
  attachment_name, # will truncate path component
232
- io.BytesIO(raw_data), # type: ignore
257
+ raw_file, # type: ignore
233
258
  content_type,
234
259
  {"Expires": "0"},
235
260
  ),
236
261
  }
237
-
238
262
  response = self.session.post(
239
263
  url,
240
264
  files=file_to_upload, # type: ignore
241
265
  headers={"X-Atlassian-Token": "no-check"},
242
266
  )
267
+ else:
268
+ raise NotImplementedError("never occurs")
243
269
 
244
270
  response.raise_for_status()
245
271
  data = response.json()
@@ -491,7 +517,7 @@ class ConfluenceSession:
491
517
  page_id = self.page_exists(title)
492
518
 
493
519
  if page_id is not None:
494
- LOGGER.debug("Retrieving existing page: %d", page_id)
520
+ LOGGER.debug("Retrieving existing page: %s", page_id)
495
521
  return self.get_page(page_id)
496
522
  else:
497
523
  LOGGER.debug("Creating new page with title: %s", title)
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import os.path
3
11
  from pathlib import Path
@@ -36,6 +44,7 @@ class Application:
36
44
  def synchronize(self, path: Path) -> None:
37
45
  "Synchronizes a single Markdown page or a directory of Markdown pages."
38
46
 
47
+ path = path.resolve(True)
39
48
  if path.is_dir():
40
49
  self.synchronize_directory(path)
41
50
  elif path.is_file():
@@ -43,15 +52,31 @@ class Application:
43
52
  else:
44
53
  raise ValueError(f"expected: valid file or directory path; got: {path}")
45
54
 
46
- def synchronize_page(self, page_path: Path) -> None:
55
+ def synchronize_page(
56
+ self, page_path: Path, root_dir: Optional[Path] = None
57
+ ) -> None:
47
58
  "Synchronizes a single Markdown page with Confluence."
48
59
 
49
- self._synchronize_page(page_path, {})
60
+ page_path = page_path.resolve(True)
61
+ if root_dir is None:
62
+ root_dir = page_path.parent
63
+ else:
64
+ root_dir = root_dir.resolve(True)
65
+
66
+ self._synchronize_page(page_path, root_dir, {})
50
67
 
51
- def synchronize_directory(self, local_dir: Path) -> None:
68
+ def synchronize_directory(
69
+ self, local_dir: Path, root_dir: Optional[Path] = None
70
+ ) -> None:
52
71
  "Synchronizes a directory of Markdown pages with Confluence."
53
72
 
54
- LOGGER.info(f"Synchronizing directory: {local_dir}")
73
+ local_dir = local_dir.resolve(True)
74
+ if root_dir is None:
75
+ root_dir = local_dir
76
+ else:
77
+ root_dir = root_dir.resolve(True)
78
+
79
+ LOGGER.info("Synchronizing directory: %s", local_dir)
55
80
 
56
81
  # Step 1: build index of all page metadata
57
82
  page_metadata: Dict[Path, ConfluencePageMetadata] = {}
@@ -61,21 +86,22 @@ class Application:
61
86
  else None
62
87
  )
63
88
  self._index_directory(local_dir, root_id, page_metadata)
64
- LOGGER.info(f"indexed {len(page_metadata)} page(s)")
89
+ LOGGER.info("Indexed %d page(s)", len(page_metadata))
65
90
 
66
91
  # Step 2: convert each page
67
92
  for page_path in page_metadata.keys():
68
- self._synchronize_page(page_path, page_metadata)
93
+ self._synchronize_page(page_path, root_dir, page_metadata)
69
94
 
70
95
  def _synchronize_page(
71
96
  self,
72
97
  page_path: Path,
98
+ root_dir: Path,
73
99
  page_metadata: Dict[Path, ConfluencePageMetadata],
74
100
  ) -> None:
75
101
  base_path = page_path.parent
76
102
 
77
- LOGGER.info(f"Synchronizing page: {page_path}")
78
- document = ConfluenceDocument(page_path, self.options, page_metadata)
103
+ LOGGER.info("Synchronizing page: %s", page_path)
104
+ document = ConfluenceDocument(page_path, self.options, root_dir, page_metadata)
79
105
 
80
106
  if document.id.space_key:
81
107
  with self.api.switch_space(document.id.space_key):
@@ -91,7 +117,7 @@ class Application:
91
117
  ) -> None:
92
118
  "Indexes Markdown files in a directory recursively."
93
119
 
94
- LOGGER.info(f"Indexing directory: {local_dir}")
120
+ LOGGER.info("Indexing directory: %s", local_dir)
95
121
 
96
122
  matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
97
123
 
@@ -107,22 +133,30 @@ class Application:
107
133
  directories.append(Path(local_dir) / entry.name)
108
134
 
109
135
  # make page act as parent node in Confluence
110
- parent_id: Optional[ConfluenceQualifiedID] = None
136
+ parent_doc: Optional[Path] = None
111
137
  if (Path(local_dir) / "index.md") in files:
112
- parent_id = read_qualified_id(Path(local_dir) / "index.md")
138
+ parent_doc = Path(local_dir) / "index.md"
113
139
  elif (Path(local_dir) / "README.md") in files:
114
- parent_id = read_qualified_id(Path(local_dir) / "README.md")
140
+ parent_doc = Path(local_dir) / "README.md"
141
+
142
+ if parent_doc is not None:
143
+ files.remove(parent_doc)
115
144
 
116
- if parent_id is None:
145
+ metadata = self._get_or_create_page(parent_doc, root_id)
146
+ LOGGER.debug("Indexed parent %s with metadata: %s", parent_doc, metadata)
147
+ page_metadata[parent_doc] = metadata
148
+
149
+ parent_id = read_qualified_id(parent_doc) or root_id
150
+ else:
117
151
  parent_id = root_id
118
152
 
119
153
  for doc in files:
120
154
  metadata = self._get_or_create_page(doc, parent_id)
121
- LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
155
+ LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
122
156
  page_metadata[doc] = metadata
123
157
 
124
158
  for directory in directories:
125
- self._index_directory(Path(local_dir) / directory, parent_id, page_metadata)
159
+ self._index_directory(directory, parent_id, page_metadata)
126
160
 
127
161
  def _get_or_create_page(
128
162
  self,
@@ -202,20 +236,19 @@ class Application:
202
236
  for image in document.images:
203
237
  self.api.upload_attachment(
204
238
  document.id.page_id,
205
- base_path / image,
206
239
  attachment_name(image),
240
+ attachment_path=base_path / image,
207
241
  )
208
242
 
209
- for image, data in document.embedded_images.items():
243
+ for name, data in document.embedded_images.items():
210
244
  self.api.upload_attachment(
211
245
  document.id.page_id,
212
- Path("EMB") / image,
213
- attachment_name(image),
246
+ name,
214
247
  raw_data=data,
215
248
  )
216
249
 
217
250
  content = document.xhtml()
218
- LOGGER.debug(f"generated Confluence Storage Format document:\n{content}")
251
+ LOGGER.debug("Generated Confluence Storage Format document:\n%s", content)
219
252
  self.api.update_page(document.id.page_id, content)
220
253
 
221
254
  def _update_markdown(
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  # mypy: disable-error-code="dict-item"
2
10
 
3
11
  import hashlib
@@ -10,7 +18,7 @@ import uuid
10
18
  import xml.etree.ElementTree
11
19
  from dataclasses import dataclass
12
20
  from pathlib import Path
13
- from typing import Any, Dict, List, Literal, Optional, Tuple
21
+ from typing import Any, Dict, List, Literal, Optional, Tuple, Union
14
22
  from urllib.parse import ParseResult, urlparse, urlunparse
15
23
 
16
24
  import lxml.etree as ET
@@ -293,9 +301,10 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
293
301
 
294
302
  options: ConfluenceConverterOptions
295
303
  path: Path
296
- base_path: Path
304
+ base_dir: Path
305
+ root_dir: Path
297
306
  links: List[str]
298
- images: List[str]
307
+ images: List[Path]
299
308
  embedded_images: Dict[str, bytes]
300
309
  page_metadata: Dict[Path, ConfluencePageMetadata]
301
310
 
@@ -303,12 +312,14 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
303
312
  self,
304
313
  options: ConfluenceConverterOptions,
305
314
  path: Path,
315
+ root_dir: Path,
306
316
  page_metadata: Dict[Path, ConfluencePageMetadata],
307
317
  ) -> None:
308
318
  super().__init__()
309
319
  self.options = options
310
320
  self.path = path
311
- self.base_path = path.parent
321
+ self.base_dir = path.parent
322
+ self.root_dir = root_dir
312
323
  self.links = []
313
324
  self.images = []
314
325
  self.embedded_images = {}
@@ -343,7 +354,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
343
354
  if is_absolute_url(url):
344
355
  return None
345
356
 
346
- LOGGER.debug(f"found link {url} relative to {self.path}")
357
+ LOGGER.debug("Found link %s relative to %s", url, self.path)
347
358
  relative_url: ParseResult = urlparse(url)
348
359
 
349
360
  if (
@@ -353,7 +364,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
353
364
  and not relative_url.params
354
365
  and not relative_url.query
355
366
  ):
356
- LOGGER.debug(f"found local URL: {url}")
367
+ LOGGER.debug("Found local URL: %s", url)
357
368
  if self.options.heading_anchors:
358
369
  # <ac:link ac:anchor="anchor"><ac:link-body>...</ac:link-body></ac:link>
359
370
  target = relative_url.fragment.lstrip("#")
@@ -375,9 +386,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
375
386
  # convert the relative URL to absolute URL based on the base path value, then look up
376
387
  # the absolute path in the page metadata dictionary to discover the relative path
377
388
  # within Confluence that should be used
378
- absolute_path = (self.base_path / relative_url.path).absolute()
379
- if not str(absolute_path).startswith(str(self.base_path)):
380
- msg = f"relative URL {url} points to outside base path: {self.base_path}"
389
+ absolute_path = (self.base_dir / relative_url.path).resolve(True)
390
+ if not str(absolute_path).startswith(str(self.root_dir)):
391
+ msg = f"relative URL {url} points to outside root path: {self.root_dir}"
381
392
  if self.options.ignore_invalid_url:
382
393
  LOGGER.warning(msg)
383
394
  anchor.attrib.pop("href")
@@ -385,8 +396,6 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
385
396
  else:
386
397
  raise DocumentError(msg)
387
398
 
388
- relative_path = os.path.relpath(absolute_path, self.base_path)
389
-
390
399
  link_metadata = self.page_metadata.get(absolute_path)
391
400
  if link_metadata is None:
392
401
  msg = f"unable to find matching page for URL: {url}"
@@ -397,8 +406,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
397
406
  else:
398
407
  raise DocumentError(msg)
399
408
 
409
+ relative_path = os.path.relpath(absolute_path, self.base_dir)
400
410
  LOGGER.debug(
401
- f"found link to page {relative_path} with metadata: {link_metadata}"
411
+ "found link to page %s with metadata: %s", relative_path, link_metadata
402
412
  )
403
413
  self.links.append(url)
404
414
 
@@ -417,24 +427,31 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
417
427
  )
418
428
  transformed_url = urlunparse(components)
419
429
 
420
- LOGGER.debug(f"transformed relative URL: {url} to URL: {transformed_url}")
430
+ LOGGER.debug("Transformed relative URL: %s to URL: %s", url, transformed_url)
421
431
  anchor.attrib["href"] = transformed_url
422
432
  return None
423
433
 
424
434
  def _transform_image(self, image: ET._Element) -> ET._Element:
425
435
  path: str = image.attrib["src"]
426
436
 
437
+ if not path:
438
+ raise DocumentError("image lacks `src` attribute")
439
+
440
+ if is_absolute_url(path):
441
+ # images whose `src` attribute is an absolute URL cannot be converted into an `ac:image`;
442
+ # Confluence images are expected to refer to an uploaded attachment
443
+ raise DocumentError("image has a `src` attribute that is an absolute URL")
444
+
445
+ relative_path = Path(path)
446
+
427
447
  # prefer PNG over SVG; Confluence displays SVG in wrong size, and text labels are truncated
428
- if path and is_relative_url(path):
429
- relative_path = Path(path)
430
- if (
431
- relative_path.suffix == ".svg"
432
- and (self.base_path / relative_path.with_suffix(".png")).exists()
433
- ):
434
- path = str(relative_path.with_suffix(".png"))
435
-
436
- self.images.append(path)
448
+ png_file = relative_path.with_suffix(".png")
449
+ if relative_path.suffix == ".svg" and (self.base_dir / png_file).exists():
450
+ relative_path = png_file
451
+
452
+ self.images.append(relative_path)
437
453
  caption = image.attrib["alt"]
454
+ image_name = attachment_name(relative_path)
438
455
  return AC(
439
456
  "image",
440
457
  {
@@ -443,7 +460,8 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
443
460
  },
444
461
  RI(
445
462
  "attachment",
446
- {ET.QName(namespaces["ri"], "filename"): attachment_name(path)},
463
+ # refers to an attachment uploaded alongside the page
464
+ {ET.QName(namespaces["ri"], "filename"): image_name},
447
465
  ),
448
466
  AC("caption", HTML.p(caption)),
449
467
  )
@@ -749,6 +767,9 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
749
767
  tail: str = child.tail
750
768
  child.tail = tail.replace("\n", " ")
751
769
 
770
+ if not isinstance(child.tag, str):
771
+ return None
772
+
752
773
  if self.options.heading_anchors:
753
774
  # <h1>...</h1>
754
775
  # <h2>...</h2> ...
@@ -924,7 +945,7 @@ class ConfluenceDocumentOptions:
924
945
  class ConfluenceDocument:
925
946
  id: ConfluenceQualifiedID
926
947
  links: List[str]
927
- images: List[str]
948
+ images: List[Path]
928
949
 
929
950
  options: ConfluenceDocumentOptions
930
951
  root: ET._Element
@@ -933,10 +954,11 @@ class ConfluenceDocument:
933
954
  self,
934
955
  path: Path,
935
956
  options: ConfluenceDocumentOptions,
957
+ root_dir: Path,
936
958
  page_metadata: Dict[Path, ConfluencePageMetadata],
937
959
  ) -> None:
938
960
  self.options = options
939
- path = path.absolute()
961
+ path = path.resolve(True)
940
962
 
941
963
  with open(path, "r", encoding="utf-8") as f:
942
964
  text = f.read()
@@ -990,6 +1012,7 @@ class ConfluenceDocument:
990
1012
  webui_links=self.options.webui_links,
991
1013
  ),
992
1014
  path,
1015
+ root_dir,
993
1016
  page_metadata,
994
1017
  )
995
1018
  converter.visit(self.root)
@@ -1001,7 +1024,7 @@ class ConfluenceDocument:
1001
1024
  return elements_to_string(self.root)
1002
1025
 
1003
1026
 
1004
- def attachment_name(name: str) -> str:
1027
+ def attachment_name(name: Union[Path, str]) -> str:
1005
1028
  """
1006
1029
  Safe name for use with attachment uploads.
1007
1030
 
@@ -1010,7 +1033,7 @@ def attachment_name(name: str) -> str:
1010
1033
  * Special characters: hyphen (-), underscore (_), period (.)
1011
1034
  """
1012
1035
 
1013
- return re.sub(r"[^\-0-9A-Za-z_.]", "_", name)
1036
+ return re.sub(r"[^\-0-9A-Za-z_.]", "_", str(name))
1014
1037
 
1015
1038
 
1016
1039
  def sanitize_confluence(html: str) -> str:
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import pathlib
2
10
 
3
11
  import pymdownx.emoji1_db as emoji_db
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import os.path
2
10
  from dataclasses import dataclass
3
11
  from fnmatch import fnmatch
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import os
3
11
  import os.path
@@ -48,11 +56,15 @@ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
48
56
  filename,
49
57
  "--outputFormat",
50
58
  output_format,
59
+ "--backgroundColor",
60
+ "transparent",
61
+ "--scale",
62
+ "2",
51
63
  ]
52
64
  root = os.path.dirname(__file__)
53
65
  if is_docker():
54
66
  cmd.extend(["-p", os.path.join(root, "puppeteer-config.json")])
55
- LOGGER.debug(f"Executing: {' '.join(cmd)}")
67
+ LOGGER.debug("Executing: %s", " ".join(cmd))
56
68
  try:
57
69
  proc = subprocess.Popen(
58
70
  cmd,
@@ -1,8 +1,16 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import hashlib
2
10
  import logging
3
11
  import os
4
12
  from pathlib import Path
5
- from typing import Dict, List
13
+ from typing import Dict, List, Optional
6
14
 
7
15
  from .converter import (
8
16
  ConfluenceDocument,
@@ -30,33 +38,56 @@ class Processor:
30
38
  def process(self, path: Path) -> None:
31
39
  "Processes a single Markdown file or a directory of Markdown files."
32
40
 
41
+ path = path.resolve(True)
33
42
  if path.is_dir():
34
43
  self.process_directory(path)
35
44
  elif path.is_file():
36
- self.process_page(path, {})
45
+ self.process_page(path)
37
46
  else:
38
47
  raise ValueError(f"expected: valid file or directory path; got: {path}")
39
48
 
40
- def process_directory(self, local_dir: Path) -> None:
49
+ def process_directory(
50
+ self, local_dir: Path, root_dir: Optional[Path] = None
51
+ ) -> None:
41
52
  "Recursively scans a directory hierarchy for Markdown files."
42
53
 
43
- LOGGER.info(f"Synchronizing directory: {local_dir}")
54
+ local_dir = local_dir.resolve(True)
55
+ if root_dir is None:
56
+ root_dir = local_dir
57
+ else:
58
+ root_dir = root_dir.resolve(True)
59
+
60
+ LOGGER.info("Synchronizing directory: %s", local_dir)
44
61
 
45
62
  # Step 1: build index of all page metadata
46
63
  page_metadata: Dict[Path, ConfluencePageMetadata] = {}
47
64
  self._index_directory(local_dir, page_metadata)
48
- LOGGER.info(f"indexed {len(page_metadata)} page(s)")
65
+ LOGGER.info("Indexed %d page(s)", len(page_metadata))
49
66
 
50
67
  # Step 2: convert each page
51
68
  for page_path in page_metadata.keys():
52
- self.process_page(page_path, page_metadata)
69
+ self._process_page(page_path, root_dir, page_metadata)
53
70
 
54
- def process_page(
55
- self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
71
+ def process_page(self, path: Path, root_dir: Optional[Path] = None) -> None:
72
+ "Processes a single Markdown file."
73
+
74
+ path = path.resolve(True)
75
+ if root_dir is None:
76
+ root_dir = path.parent
77
+ else:
78
+ root_dir = root_dir.resolve(True)
79
+
80
+ self._process_page(path, root_dir, {})
81
+
82
+ def _process_page(
83
+ self,
84
+ path: Path,
85
+ root_dir: Path,
86
+ page_metadata: Dict[Path, ConfluencePageMetadata],
56
87
  ) -> None:
57
88
  "Processes a single Markdown file."
58
89
 
59
- document = ConfluenceDocument(path, self.options, page_metadata)
90
+ document = ConfluenceDocument(path, self.options, root_dir, page_metadata)
60
91
  content = document.xhtml()
61
92
  with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
62
93
  f.write(content)
@@ -68,7 +99,7 @@ class Processor:
68
99
  ) -> None:
69
100
  "Indexes Markdown files in a directory recursively."
70
101
 
71
- LOGGER.info(f"Indexing directory: {local_dir}")
102
+ LOGGER.info("Indexing directory: %s", local_dir)
72
103
 
73
104
  matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
74
105
 
@@ -85,11 +116,11 @@ class Processor:
85
116
 
86
117
  for doc in files:
87
118
  metadata = self._get_page(doc)
88
- LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
119
+ LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
89
120
  page_metadata[doc] = metadata
90
121
 
91
122
  for directory in directories:
92
- self._index_directory(Path(local_dir) / directory, page_metadata)
123
+ self._index_directory(directory, page_metadata)
93
124
 
94
125
  def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
95
126
  "Extracts metadata from a Markdown file."
@@ -102,7 +133,7 @@ class Processor:
102
133
  if self.options.root_page_id is not None:
103
134
  hash = hashlib.md5(document.encode("utf-8"))
104
135
  digest = "".join(f"{c:x}" for c in hash.digest())
105
- LOGGER.info(f"Identifier '{digest}' assigned to page: {absolute_path}")
136
+ LOGGER.info("Identifier %s assigned to page: %s", digest, absolute_path)
106
137
  qualified_id = ConfluenceQualifiedID(digest)
107
138
  else:
108
139
  raise ValueError("required: page ID for local output")
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import os
2
10
  from typing import Dict, Optional
3
11
 
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import sys
2
10
 
3
11
  if sys.version_info >= (3, 9):
@@ -29,10 +29,10 @@ packages = find:
29
29
  python_requires = >=3.8
30
30
  install_requires =
31
31
  lxml >= 5.3
32
- types-lxml >= 2024.8.7
33
- markdown >= 3.6
34
- types-markdown >= 3.6
35
- pymdown-extensions >= 10.9
32
+ types-lxml >= 2024.11.8
33
+ markdown >= 3.7
34
+ types-markdown >= 3.7
35
+ pymdown-extensions >= 10.12
36
36
  pyyaml >= 6.0
37
37
  types-PyYAML >= 6.0
38
38
  requests >= 2.32
@@ -0,0 +1,12 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ from setuptools import setup
10
+
11
+ if __name__ == "__main__":
12
+ setup()
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import os
3
11
  import os.path
@@ -65,7 +73,8 @@ class TestConversion(unittest.TestCase):
65
73
  with self.subTest(name=name):
66
74
  actual = ConfluenceDocument(
67
75
  self.source_dir / f"{name}.md",
68
- ConfluenceDocumentOptions(ignore_invalid_url=True),
76
+ ConfluenceDocumentOptions(),
77
+ self.source_dir,
69
78
  {},
70
79
  ).xhtml()
71
80
  actual = standardize(actual)
@@ -75,10 +84,25 @@ class TestConversion(unittest.TestCase):
75
84
 
76
85
  self.assertEqual(actual, expected)
77
86
 
87
+ def test_broken_links(self) -> None:
88
+ actual = ConfluenceDocument(
89
+ self.source_dir / "missing.md",
90
+ ConfluenceDocumentOptions(ignore_invalid_url=True),
91
+ self.source_dir,
92
+ {},
93
+ ).xhtml()
94
+ actual = standardize(actual)
95
+
96
+ with open(self.target_dir / "missing.xml", "r", encoding="utf-8") as f:
97
+ expected = canonicalize(f.read())
98
+
99
+ self.assertEqual(actual, expected)
100
+
78
101
  def test_heading_anchors(self) -> None:
79
102
  actual = ConfluenceDocument(
80
103
  self.source_dir / "anchors.md",
81
104
  ConfluenceDocumentOptions(heading_anchors=True),
105
+ self.source_dir,
82
106
  {},
83
107
  ).xhtml()
84
108
  actual = standardize(actual)
@@ -93,10 +117,10 @@ class TestConversion(unittest.TestCase):
93
117
  document = ConfluenceDocument(
94
118
  self.source_dir / "mermaid.md",
95
119
  ConfluenceDocumentOptions(
96
- ignore_invalid_url=True,
97
120
  render_mermaid=True,
98
121
  diagram_output_format="svg",
99
122
  ),
123
+ self.source_dir,
100
124
  {},
101
125
  )
102
126
  self.assertEqual(len(document.embedded_images), 6)
@@ -106,10 +130,10 @@ class TestConversion(unittest.TestCase):
106
130
  document = ConfluenceDocument(
107
131
  self.source_dir / "mermaid.md",
108
132
  ConfluenceDocumentOptions(
109
- ignore_invalid_url=True,
110
133
  render_mermaid=True,
111
134
  diagram_output_format="png",
112
135
  ),
136
+ self.source_dir,
113
137
  {},
114
138
  )
115
139
  self.assertEqual(len(document.embedded_images), 6)
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import os
3
11
  import os.path
@@ -36,6 +44,7 @@ class TestMatcher(unittest.TestCase):
36
44
  ]
37
45
  expected.remove(Entry("ignore.md", False))
38
46
  expected.remove(Entry("anchors.md", False))
47
+ expected.remove(Entry("missing.md", False))
39
48
 
40
49
  options = MatcherOptions(".mdignore", ".md")
41
50
  matcher = Matcher(options, directory)
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import os
3
11
  import shutil
@@ -6,7 +14,6 @@ from pathlib import Path
6
14
 
7
15
  from md2conf.mermaid import has_mmdc, render
8
16
 
9
-
10
17
  logging.basicConfig(
11
18
  level=logging.INFO,
12
19
  format="%(asctime)s - %(levelname)s - %(funcName)s [%(lineno)d] - %(message)s",
@@ -1,3 +1,11 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2024, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
1
9
  import logging
2
10
  import shutil
3
11
  import unittest
@@ -31,7 +39,6 @@ class TestProcessor(unittest.TestCase):
31
39
 
32
40
  def test_process_document(self) -> None:
33
41
  options = ConfluenceDocumentOptions(
34
- ignore_invalid_url=False,
35
42
  generated_by="Test Case",
36
43
  root_page_id="None",
37
44
  )
@@ -45,7 +52,6 @@ class TestProcessor(unittest.TestCase):
45
52
 
46
53
  def test_process_directory(self) -> None:
47
54
  options = ConfluenceDocumentOptions(
48
- ignore_invalid_url=True,
49
55
  generated_by="The Author",
50
56
  root_page_id="ROOT_PAGE_ID",
51
57
  )
@@ -1,9 +0,0 @@
1
- lxml>=5.3
2
- types-lxml>=2024.8.7
3
- markdown>=3.6
4
- types-markdown>=3.6
5
- pymdown-extensions>=10.9
6
- pyyaml>=6.0
7
- types-PyYAML>=6.0
8
- requests>=2.32
9
- types-requests>=2.32
@@ -1,4 +0,0 @@
1
- from setuptools import setup
2
-
3
- if __name__ == "__main__":
4
- setup()