markitdown-cosense 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kazu728
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: markitdown-cosense
3
+ Version: 0.1.0
4
+ Summary: A markitdown plugin for converting Scrapbox notation to Markdown
5
+ Author-email: kazu728 <kazuki.matsuo.728@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kazu728/markitdown-cosense
8
+ Project-URL: Repository, https://github.com/kazu728/markitdown-cosense
9
+ Project-URL: Issues, https://github.com/kazu728/markitdown-cosense/issues
10
+ Keywords: markitdown,scrapbox,markdown,converter,plugin
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: markitdown>=0.1.1
15
+ Dynamic: license-file
16
+
17
+ # markitdown-cosense
18
+
19
+ A [markitdown](https://github.com/microsoft/markitdown) plugin for converting Scrapbox notation to Markdown.
20
+
21
+ ## Features
22
+
23
+ This plugin converts various Scrapbox notations to standard Markdown format:
24
+
25
+ - **Headings**: `[* Heading]` → `# Heading`
26
+ - **Text decorations**: `[/ italic]`, `[- strikethrough]`, `[** bold]`
27
+ - **Lists**: Indented lines with spaces, tabs, or full-width spaces
28
+ - **Code blocks**: `code:language` notation
29
+ - **Tables**: `table:name` notation
30
+ - **Links and images**: `[title url]`, `[img url]`
31
+ - **Math expressions**: `[$ formula $]` → `$formula$`
32
+ - **LaTeX blocks**: `code:tex` with mathematical content
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install markitdown-cosense
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### With markitdown CLI
43
+
44
+ ```bash
45
+ # List available plugins
46
+ markitdown --list-plugins
47
+
48
+ # Convert a file using the plugin
49
+ markitdown --use-plugins your-file.txt
50
+ ```
51
+
52
+ ### Programmatic usage
53
+
54
+ ```python
55
+ from markitdown import MarkItDown
56
+ from markitdown_cosense import register_converters
57
+
58
+ # Initialize markitdown
59
+ md = MarkItDown()
60
+
61
+ # Register the converter
62
+ register_converters(md)
63
+
64
+ # Convert a file
65
+ result = md.convert("your-scrapbox-file.txt")
66
+ print(result.text_content)
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Converts Scrapbox notation to standard Markdown
72
+ - Handles headings, formatting, links, images, lists, tables, code blocks, and math notation
73
+ - Converts tags `[tag]` to HTML comments `<!-- tag: tag -->` to preserve information while maintaining valid Markdown
74
+
75
+ ## Examples
76
+
77
+ ### Input (Scrapbox notation)
78
+
79
+ ```
80
+ [* Project Title]
81
+
82
+ [** Overview]
83
+ This project is about [/ converting] Scrapbox notation.
84
+
85
+ Features:
86
+ Main feature
87
+ Sub feature 1
88
+ Sub feature 2
89
+ Another feature
90
+
91
+ code:python
92
+ def hello():
93
+ print("Hello, World!")
94
+
95
+ table:Results
96
+ Name Score Grade
97
+ Alice 95 A
98
+ Bob 87 B
99
+ ```
100
+
101
+ ### Output (Markdown)
102
+
103
+ ```markdown
104
+ # Project Title
105
+
106
+ ## Overview
107
+ This project is about *converting* Scrapbox notation.
108
+
109
+ Features:
110
+ - Main feature
111
+ - Sub feature 1
112
+ - Sub feature 2
113
+ - Another feature
114
+
115
+ ```python
116
+ def hello():
117
+ print("Hello, World!")
118
+ ```
119
+
120
+ ## Results
121
+
122
+ | Name | Score | Grade |
123
+ |---|---|---|
124
+ | Alice | 95 | A |
125
+ | Bob | 87 | B |
126
+ ```
@@ -0,0 +1,110 @@
1
+ # markitdown-cosense
2
+
3
+ A [markitdown](https://github.com/microsoft/markitdown) plugin for converting Scrapbox notation to Markdown.
4
+
5
+ ## Features
6
+
7
+ This plugin converts various Scrapbox notations to standard Markdown format:
8
+
9
+ - **Headings**: `[* Heading]` → `# Heading`
10
+ - **Text decorations**: `[/ italic]`, `[- strikethrough]`, `[** bold]`
11
+ - **Lists**: Indented lines with spaces, tabs, or full-width spaces
12
+ - **Code blocks**: `code:language` notation
13
+ - **Tables**: `table:name` notation
14
+ - **Links and images**: `[title url]`, `[img url]`
15
+ - **Math expressions**: `[$ formula $]` → `$formula$`
16
+ - **LaTeX blocks**: `code:tex` with mathematical content
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install markitdown-cosense
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### With markitdown CLI
27
+
28
+ ```bash
29
+ # List available plugins
30
+ markitdown --list-plugins
31
+
32
+ # Convert a file using the plugin
33
+ markitdown --use-plugins your-file.txt
34
+ ```
35
+
36
+ ### Programmatic usage
37
+
38
+ ```python
39
+ from markitdown import MarkItDown
40
+ from markitdown_cosense import register_converters
41
+
42
+ # Initialize markitdown
43
+ md = MarkItDown()
44
+
45
+ # Register the converter
46
+ register_converters(md)
47
+
48
+ # Convert a file
49
+ result = md.convert("your-scrapbox-file.txt")
50
+ print(result.text_content)
51
+ ```
52
+
53
+ ## Features
54
+
55
+ - Converts Scrapbox notation to standard Markdown
56
+ - Handles headings, formatting, links, images, lists, tables, code blocks, and math notation
57
+ - Converts tags `[tag]` to HTML comments `<!-- tag: tag -->` to preserve information while maintaining valid Markdown
58
+
59
+ ## Examples
60
+
61
+ ### Input (Scrapbox notation)
62
+
63
+ ```
64
+ [* Project Title]
65
+
66
+ [** Overview]
67
+ This project is about [/ converting] Scrapbox notation.
68
+
69
+ Features:
70
+ Main feature
71
+ Sub feature 1
72
+ Sub feature 2
73
+ Another feature
74
+
75
+ code:python
76
+ def hello():
77
+ print("Hello, World!")
78
+
79
+ table:Results
80
+ Name Score Grade
81
+ Alice 95 A
82
+ Bob 87 B
83
+ ```
84
+
85
+ ### Output (Markdown)
86
+
87
+ ```markdown
88
+ # Project Title
89
+
90
+ ## Overview
91
+ This project is about *converting* Scrapbox notation.
92
+
93
+ Features:
94
+ - Main feature
95
+ - Sub feature 1
96
+ - Sub feature 2
97
+ - Another feature
98
+
99
+ ```python
100
+ def hello():
101
+ print("Hello, World!")
102
+ ```
103
+
104
+ ## Results
105
+
106
+ | Name | Score | Grade |
107
+ |---|---|---|
108
+ | Alice | 95 | A |
109
+ | Bob | 87 | B |
110
+ ```
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "markitdown-cosense"
7
+ version = "0.1.0"
8
+ description = "A markitdown plugin for converting Scrapbox notation to Markdown"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ { name = "kazu728", email = "kazuki.matsuo.728@gmail.com" }
13
+ ]
14
+ keywords = ["markitdown", "scrapbox", "markdown", "converter", "plugin"]
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ "markitdown>=0.1.1",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/kazu728/markitdown-cosense"
22
+ Repository = "https://github.com/kazu728/markitdown-cosense"
23
+ Issues = "https://github.com/kazu728/markitdown-cosense/issues"
24
+
25
+ [project.entry-points."markitdown.plugin"]
26
+ markitdown-cosense = "markitdown_cosense"
27
+
28
+ [tool.uv]
29
+ dev-dependencies = [
30
+ "pytest>=8.3.5",
31
+ "ruff>=0.11.12",
32
+ "pytest-cov>=4.0.0",
33
+ "pyright>=1.1.390",
34
+ ]
35
+
36
+ [tool.pyright]
37
+ include = ["src"]
38
+ exclude = ["**/node_modules", "**/__pycache__", "**/.*"]
39
+ pythonVersion = "3.10"
40
+ pythonPlatform = "All"
41
+ typeCheckingMode = "basic"
42
+ useLibraryCodeForTypes = true
43
+ reportMissingImports = "warning"
44
+ reportMissingTypeStubs = false
45
+ reportImportCycles = "error"
46
+ reportUnusedImport = "warning"
47
+ reportUnusedClass = "warning"
48
+ reportUnusedFunction = "warning"
49
+ reportUnusedVariable = "warning"
50
+
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.pytest.ini_options]
56
+ pythonpath = ["src"]
57
+ testpaths = ["tests"]
58
+
59
+ [tool.ruff.lint]
60
+ select = [
61
+ "I", # isort
62
+ "F", # pyflakes
63
+ ]
64
+
65
+ [tool.ruff.format]
66
+ quote-style = "double"
67
+ indent-style = "space"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """markitdown-cosense: A markitdown plugin for converting Scrapbox to Markdown."""
2
+
3
+ from ._plugin import __plugin_interface_version__, register_converters
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = [
7
+ "register_converters",
8
+ "__plugin_interface_version__",
9
+ "__version__",
10
+ ]
@@ -0,0 +1,366 @@
1
+ import re
2
+ from typing import (
3
+ Any,
4
+ BinaryIO,
5
+ Dict,
6
+ List,
7
+ Tuple,
8
+ )
9
+
10
+ from markitdown import (
11
+ DocumentConverter,
12
+ DocumentConverterResult,
13
+ MarkItDown,
14
+ StreamInfo,
15
+ )
16
+
17
+ __plugin_interface_version__ = 1
18
+
19
+
20
+ ACCEPTED_FILE_EXTENSIONS = [".txt"]
21
+ IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp"]
22
+ CODE_BLOCK_PREFIX = "code:"
23
+ CODE_BLOCK_PLACEHOLDER = "<<<CODEBLOCK{}>>>"
24
+ CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
25
+
26
+ _image_extensions_pattern = None
27
+
28
+
29
+ def register_converters(markitdown: MarkItDown, **kwargs) -> None:
30
+ markitdown.register_converter(MarkdownConverter())
31
+
32
+
33
+ class MarkdownConverter(DocumentConverter):
34
+ def accepts(
35
+ self, _file_stream: BinaryIO, stream_info: StreamInfo, **kwargs
36
+ ) -> bool:
37
+ extension = (stream_info.extension or "").lower()
38
+ return extension in ACCEPTED_FILE_EXTENSIONS
39
+
40
+ def convert(
41
+ self, file_stream: BinaryIO, stream_info: StreamInfo, **kwargs
42
+ ) -> DocumentConverterResult:
43
+ file_stream.seek(0)
44
+ content = file_stream.read().decode("utf-8")
45
+
46
+ return DocumentConverterResult(_convert_content(content))
47
+
48
+
49
+ def _convert_content(content: str) -> str:
50
+ content = convert_code_blocks(content)
51
+ content, protected_blocks = protect_code_blocks(content)
52
+
53
+ content = convert_tables(content)
54
+ content = convert_lists(content)
55
+ content = apply_conversions(content)
56
+
57
+ return restore_code_blocks(content, protected_blocks)
58
+
59
+
60
+ def convert_code_blocks(content: str) -> str:
61
+ lines = content.splitlines()
62
+ result = []
63
+ i = 0
64
+
65
+ while i < len(lines):
66
+ line = lines[i]
67
+ stripped = line.lstrip(" \t ")
68
+
69
+ if stripped.startswith(CODE_BLOCK_PREFIX):
70
+ indent = line[: len(line) - len(stripped)]
71
+ filename = stripped[len(CODE_BLOCK_PREFIX) :].strip()
72
+
73
+ code_lines, next_i = _collect_code_block_lines(lines, i + 1)
74
+
75
+ if filename == "tex":
76
+ _process_latex_block(code_lines, result, indent)
77
+ else:
78
+ if filename:
79
+ parts = filename.rsplit(".", 1)
80
+ lang = parts[1] if len(parts) == 2 else filename
81
+ else:
82
+ lang = ""
83
+ _add_code_block(result, lang, code_lines, bool(indent))
84
+
85
+ i = next_i
86
+ else:
87
+ result.append(line)
88
+ i += 1
89
+
90
+ return "\n".join(result)
91
+
92
+
93
+ def _collect_code_block_lines(
94
+ lines: List[str], start_index: int
95
+ ) -> Tuple[List[str], int]:
96
+ code_lines = []
97
+ base_indent = (
98
+ calculate_base_indentation(lines[start_index])
99
+ if start_index < len(lines) and lines[start_index].strip()
100
+ else 0
101
+ )
102
+ consecutive_empty_lines = 0
103
+
104
+ for i in range(start_index, len(lines)):
105
+ line = lines[i]
106
+
107
+ # Count consecutive empty lines
108
+ if not line.strip():
109
+ consecutive_empty_lines += 1
110
+ # End block after 2 consecutive empty lines
111
+ if consecutive_empty_lines >= 2:
112
+ return code_lines[:-1], i # Remove the last empty line
113
+ else:
114
+ consecutive_empty_lines = 0
115
+
116
+ # Check for block end conditions
117
+ if line.strip() and (
118
+ calculate_base_indentation(line) < base_indent
119
+ or line.lstrip(" \t ").startswith(CODE_BLOCK_PREFIX)
120
+ ):
121
+ return code_lines, i
122
+
123
+ code_lines.append(line)
124
+
125
+ return code_lines, len(lines)
126
+
127
+
128
+ def protect_code_blocks(content: str) -> Tuple[str, List[str]]:
129
+ code_blocks = CODE_BLOCK_PATTERN.findall(content)
130
+
131
+ def replacer(match):
132
+ index = replacer.counter
133
+ replacer.counter += 1
134
+ return CODE_BLOCK_PLACEHOLDER.format(index)
135
+
136
+ replacer.counter = 0
137
+ protected_content = CODE_BLOCK_PATTERN.sub(replacer, content)
138
+
139
+ return protected_content, code_blocks
140
+
141
+
142
+ def restore_code_blocks(content: str, code_blocks: List[str]) -> str:
143
+ for i, code_block in enumerate(code_blocks):
144
+ content = content.replace(CODE_BLOCK_PLACEHOLDER.format(i), code_block)
145
+ return content
146
+
147
+
148
+ def get_image_extensions_pattern() -> re.Pattern[str]:
149
+ global _image_extensions_pattern
150
+ if _image_extensions_pattern is None:
151
+ extensions = "|".join(IMAGE_EXTENSIONS)
152
+ _image_extensions_pattern = re.compile(
153
+ rf"\[(https?://[^\s\]]+\.(?:{extensions}))\]"
154
+ )
155
+ return _image_extensions_pattern
156
+
157
+
158
+ def calculate_base_indentation(line: str) -> int:
159
+ for i, char in enumerate(line):
160
+ if char not in (" ", "\t", " "):
161
+ return i
162
+ return len(line)
163
+
164
+
165
+ def is_indented_line(line: str) -> bool:
166
+ return bool(line and line[0] in (" ", "\t", " "))
167
+
168
+
169
+ CONVERSION_PATTERNS = [
170
+ # Tag pattern - must come first to avoid conflicts
171
+ (
172
+ r"\[(?!\*|img\s|/\s|-\s|\$|https?://|YouTube\s|Twitter\s|\w+\s+https?://)([^\[\]/\-\*\s][^\[\]/\-\*\]]*?)(?!\s+https?://)\]",
173
+ r"<!-- tag: \1 -->",
174
+ ),
175
+ # Formatting patterns
176
+ (r"\[\*/\s*(.*?)\]", r"***\1***"),
177
+ (r"\[\*-\s*(.*?)\]", r"**~~\1~~**"),
178
+ (r"\[/-\s*(.*?)\]", r"*~~\1~~*"),
179
+ (r"\[\*\*\*\s*(.*?)\s*\*\*\*\]", r"**\1**"),
180
+ (r"\[\*\*\s*(.*?)\s*\*\*\]", r"**\1**"),
181
+ # Heading patterns
182
+ (r"\[\*\*\*\*\*\s*(.*?)\]", r"##### \1"),
183
+ (r"\[\*\*\*\*\s*(.*?)\]", r"#### \1"),
184
+ (r"\[\*\*\*\s*(.*?)\]", r"### \1"),
185
+ (r"\[\*\*\s*(.*?)\]", r"## \1"),
186
+ (r"\[\*\s*(.*?)\]", r"# \1"),
187
+ # Basic formatting
188
+ (r"\[/\s*(.*?)\]", r"*\1*"),
189
+ (r"\[-\s*(.*?)\]", r"~~\1~~"),
190
+ (r"\[\$\s*(.*?)\s*\$\]", r"$\1$"),
191
+ # Images and media
192
+ (r"\[img\s+(https?://[^\s\]]+)\]", r"![img](\1)"),
193
+ ("_DYNAMIC_IMAGE_PATTERN_", r"![](\1)"), # Special marker for dynamic pattern
194
+ (
195
+ r"\[YouTube\s+(https?://(?:www\.)?youtube\.com/watch\?v=[\w-]+|https?://youtu\.be/[\w-]+)\]",
196
+ r"[YouTube Video](\1)",
197
+ ),
198
+ (
199
+ r"\[Twitter\s+(https?://(?:www\.)?twitter\.com/\w+/status/\d+|https?://x\.com/\w+/status/\d+)\]",
200
+ r"[Twitter Post](\1)",
201
+ ),
202
+ # Links
203
+ (r"\[([^/\-\*\]]+?)\s+(https?://[^\s\]]+)\]", r"[\1](\2)"),
204
+ (r"\[(https?://[^\s\]]+)\s+([^/\-\*\]]+?)\]", r"[\2](\1)"),
205
+ # URL auto-linking
206
+ (r'(?<!\()(https?://[^\s<>"\']+(?:\([^\s<>"\']*\)|[^\s<>"\']*)*)', r"<\1>"),
207
+ # Blockquotes
208
+ (r"^>\s*(.*)$", r"> \1"),
209
+ ]
210
+
211
+ _compiled_patterns: List[Tuple[re.Pattern[str], str]] | None = None
212
+
213
+
214
+ def _get_compiled_patterns() -> List[Tuple[re.Pattern[str], str]]:
215
+ global _compiled_patterns
216
+ if _compiled_patterns is None:
217
+ compiled = []
218
+ for pattern_str, replacement in CONVERSION_PATTERNS:
219
+ if pattern_str == "_DYNAMIC_IMAGE_PATTERN_":
220
+ pattern = get_image_extensions_pattern()
221
+ else:
222
+ pattern = re.compile(pattern_str, re.MULTILINE)
223
+ compiled.append((pattern, replacement))
224
+ _compiled_patterns = compiled
225
+ return _compiled_patterns
226
+
227
+
228
+ def apply_conversions(content: str) -> str:
229
+ for pattern, replacement in _get_compiled_patterns():
230
+ content = pattern.sub(replacement, content)
231
+ return content
232
+
233
+
234
+ def _process_latex_block(
235
+ code_lines: List[str], result: List[str], leading_indent: str
236
+ ) -> None:
237
+ math_operators = set("=+-*/^")
238
+ math_indicators = ["E(", "V(", "Cov(", "σ", "μ", "√", "Φ", "\\", "^2", "_"]
239
+ excluded_prefixes = ("![", "http", "<http", "->")
240
+
241
+ for code_line in code_lines:
242
+ stripped_line = code_line.strip()
243
+ if not stripped_line:
244
+ result.append("")
245
+ continue
246
+
247
+ has_math = any(c in stripped_line for c in math_operators) or any(
248
+ indicator in stripped_line for indicator in math_indicators
249
+ )
250
+
251
+ if (
252
+ not has_math
253
+ or stripped_line.startswith(excluded_prefixes)
254
+ or stripped_line == "code:tex"
255
+ ):
256
+ result.append(code_line.rstrip())
257
+ continue
258
+
259
+ has_japanese = any(
260
+ "\u3040" <= c <= "\u309f"
261
+ or "\u30a0" <= c <= "\u30ff"
262
+ or "\u4e00" <= c <= "\u9faf"
263
+ for c in stripped_line
264
+ )
265
+
266
+ if has_japanese:
267
+ result.append(code_line.rstrip())
268
+ else:
269
+ result.append(f"{leading_indent}${stripped_line}$")
270
+
271
+
272
+ def _add_code_block(
273
+ result: List[str],
274
+ lang: str,
275
+ code_lines: List[str],
276
+ has_leading_indent: bool = False,
277
+ ) -> None:
278
+ if has_leading_indent:
279
+ result.append("")
280
+
281
+ result.append(f"```{lang}")
282
+ result.extend(line if line.strip() else "" for line in code_lines)
283
+ result.append("```")
284
+
285
+ if has_leading_indent:
286
+ result.append("")
287
+
288
+
289
+ def convert_lists(content: str) -> str:
290
+ lines = content.splitlines()
291
+ result = []
292
+
293
+ for line in lines:
294
+ if is_indented_line(line):
295
+ indent_chars = 0
296
+ for char in line:
297
+ if char in (" ", "\t", " "):
298
+ indent_chars += 1
299
+ else:
300
+ break
301
+
302
+ indent_level = max(0, indent_chars - 1)
303
+ content = line[indent_chars:]
304
+ markdown_indent = " " * (2 * indent_level)
305
+ result.append(f"{markdown_indent}- {content}")
306
+ else:
307
+ result.append(line)
308
+
309
+ return "\n".join(result)
310
+
311
+
312
+ def convert_tables(content: str) -> str:
313
+ lines = content.splitlines()
314
+ result = []
315
+ i = 0
316
+
317
+ while i < len(lines):
318
+ line = lines[i]
319
+
320
+ if line.startswith("table:"):
321
+ processed_table = _process_table_block(lines, i)
322
+ result.extend(processed_table["content"])
323
+ i = processed_table["next_index"]
324
+ continue
325
+
326
+ result.append(line)
327
+ i += 1
328
+
329
+ return "\n".join(result)
330
+
331
+
332
+ def _process_table_block(lines: List[str], start_index: int) -> Dict[str, Any]:
333
+ line = lines[start_index]
334
+ result_content = []
335
+
336
+ table_name = line[6:].strip()
337
+ if table_name:
338
+ result_content.append(f"## {table_name}")
339
+ result_content.append("")
340
+
341
+ table_rows = []
342
+ i = start_index + 1
343
+ while i < len(lines) and lines[i].startswith((" ", "\t")):
344
+ row_content = lines[i].strip()
345
+ if row_content:
346
+ table_rows.append(row_content)
347
+ i += 1
348
+
349
+ if table_rows:
350
+ header_row = table_rows[0]
351
+ columns = [col.strip() for col in header_row.split()]
352
+
353
+ if columns:
354
+ result_content.append("| " + " | ".join(columns) + " |")
355
+ result_content.append("|" + "---|" * len(columns))
356
+
357
+ for row in table_rows[1:]:
358
+ data_columns = [col.strip() for col in row.split()]
359
+ while len(data_columns) < len(columns):
360
+ data_columns.append("")
361
+ data_columns = data_columns[: len(columns)]
362
+ result_content.append("| " + " | ".join(data_columns) + " |")
363
+
364
+ result_content.append("")
365
+
366
+ return {"content": result_content, "next_index": i}
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: markitdown-cosense
3
+ Version: 0.1.0
4
+ Summary: A markitdown plugin for converting Scrapbox notation to Markdown
5
+ Author-email: kazu728 <kazuki.matsuo.728@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kazu728/markitdown-cosense
8
+ Project-URL: Repository, https://github.com/kazu728/markitdown-cosense
9
+ Project-URL: Issues, https://github.com/kazu728/markitdown-cosense/issues
10
+ Keywords: markitdown,scrapbox,markdown,converter,plugin
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: markitdown>=0.1.1
15
+ Dynamic: license-file
16
+
17
+ # markitdown-cosense
18
+
19
+ A [markitdown](https://github.com/microsoft/markitdown) plugin for converting Scrapbox notation to Markdown.
20
+
21
+ ## Features
22
+
23
+ This plugin converts various Scrapbox notations to standard Markdown format:
24
+
25
+ - **Headings**: `[* Heading]` → `# Heading`
26
+ - **Text decorations**: `[/ italic]`, `[- strikethrough]`, `[** bold]`
27
+ - **Lists**: Indented lines with spaces, tabs, or full-width spaces
28
+ - **Code blocks**: `code:language` notation
29
+ - **Tables**: `table:name` notation
30
+ - **Links and images**: `[title url]`, `[img url]`
31
+ - **Math expressions**: `[$ formula $]` → `$formula$`
32
+ - **LaTeX blocks**: `code:tex` with mathematical content
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install markitdown-cosense
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### With markitdown CLI
43
+
44
+ ```bash
45
+ # List available plugins
46
+ markitdown --list-plugins
47
+
48
+ # Convert a file using the plugin
49
+ markitdown --use-plugins your-file.txt
50
+ ```
51
+
52
+ ### Programmatic usage
53
+
54
+ ```python
55
+ from markitdown import MarkItDown
56
+ from markitdown_cosense import register_converters
57
+
58
+ # Initialize markitdown
59
+ md = MarkItDown()
60
+
61
+ # Register the converter
62
+ register_converters(md)
63
+
64
+ # Convert a file
65
+ result = md.convert("your-scrapbox-file.txt")
66
+ print(result.text_content)
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Converts Scrapbox notation to standard Markdown
72
+ - Handles headings, formatting, links, images, lists, tables, code blocks, and math notation
73
+ - Converts tags `[tag]` to HTML comments `<!-- tag: tag -->` to preserve information while maintaining valid Markdown
74
+
75
+ ## Examples
76
+
77
+ ### Input (Scrapbox notation)
78
+
79
+ ```
80
+ [* Project Title]
81
+
82
+ [** Overview]
83
+ This project is about [/ converting] Scrapbox notation.
84
+
85
+ Features:
86
+ Main feature
87
+ Sub feature 1
88
+ Sub feature 2
89
+ Another feature
90
+
91
+ code:python
92
+ def hello():
93
+ print("Hello, World!")
94
+
95
+ table:Results
96
+ Name Score Grade
97
+ Alice 95 A
98
+ Bob 87 B
99
+ ```
100
+
101
+ ### Output (Markdown)
102
+
103
+ ```markdown
104
+ # Project Title
105
+
106
+ ## Overview
107
+ This project is about *converting* Scrapbox notation.
108
+
109
+ Features:
110
+ - Main feature
111
+ - Sub feature 1
112
+ - Sub feature 2
113
+ - Another feature
114
+
115
+ ```python
116
+ def hello():
117
+ print("Hello, World!")
118
+ ```
119
+
120
+ ## Results
121
+
122
+ | Name | Score | Grade |
123
+ |---|---|---|
124
+ | Alice | 95 | A |
125
+ | Bob | 87 | B |
126
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/markitdown_cosense/__init__.py
5
+ src/markitdown_cosense/_plugin.py
6
+ src/markitdown_cosense.egg-info/PKG-INFO
7
+ src/markitdown_cosense.egg-info/SOURCES.txt
8
+ src/markitdown_cosense.egg-info/dependency_links.txt
9
+ src/markitdown_cosense.egg-info/entry_points.txt
10
+ src/markitdown_cosense.egg-info/requires.txt
11
+ src/markitdown_cosense.egg-info/top_level.txt
12
+ tests/test_plugin.py
@@ -0,0 +1,2 @@
1
+ [markitdown.plugin]
2
+ markitdown-cosense = markitdown_cosense
@@ -0,0 +1 @@
1
+ markitdown>=0.1.1
@@ -0,0 +1 @@
1
+ markitdown_cosense
@@ -0,0 +1,273 @@
1
+ """Unified test file for markitdown-cosense plugin."""
2
+
3
+ import io
4
+ import os
5
+ import re
6
+ import tempfile
7
+
8
+ import pytest
9
+ from markitdown import MarkItDown, StreamInfo
10
+
11
+ from markitdown_cosense import register_converters
12
+ from markitdown_cosense._plugin import (
13
+ MarkdownConverter,
14
+ apply_conversions,
15
+ convert_code_blocks,
16
+ convert_lists,
17
+ convert_tables,
18
+ protect_code_blocks,
19
+ restore_code_blocks,
20
+ )
21
+
22
+
23
+ class TestPatternProcessor:
24
+ @pytest.mark.parametrize(
25
+ "input_text,expected",
26
+ [
27
+ ("[* Heading]", "# Heading"),
28
+ ("[** Heading]", "## Heading"),
29
+ ("[*** Heading]", "### Heading"),
30
+ ("[**** Heading]", "#### Heading"),
31
+ ("[***** Heading]", "##### Heading"),
32
+ ("[/ italic text]", "*italic text*"),
33
+ ("[- strikethrough text]", "~~strikethrough text~~"),
34
+ ("[** bold text **]", "**bold text**"),
35
+ ("[*** bold text ***]", "**bold text**"),
36
+ ("[*/ bold italic]", "***bold italic***"),
37
+ ("[*- bold strikethrough]", "**~~bold strikethrough~~**"),
38
+ ("[/- italic strikethrough]", "*~~italic strikethrough~~*"),
39
+ ("[$ E = mc^2 $]", "$E = mc^2$"),
40
+ (
41
+ "[YouTube https://www.youtube.com/watch?v=dQw4w9WgXcQ]",
42
+ "[YouTube Video](https://www.youtube.com/watch?v=dQw4w9WgXcQ)",
43
+ ),
44
+ (
45
+ "[Twitter https://twitter.com/user/status/123456789]",
46
+ "[Twitter Post](https://twitter.com/user/status/123456789)",
47
+ ),
48
+ ("[Link Title https://example.com]", "[Link Title](https://example.com)"),
49
+ ("[https://example.com Link Title]", "[Link Title](https://example.com)"),
50
+ ("[https://example.com/image.jpg]", "![](https://example.com/image.jpg)"),
51
+ (
52
+ "[img https://example.com/image.jpg]",
53
+ "![img](https://example.com/image.jpg)",
54
+ ),
55
+ ("Check https://example.com", "Check <https://example.com>"),
56
+ ("> This is a quote", "> This is a quote"),
57
+ ],
58
+ )
59
+ def test_basic_conversions(self, input_text, expected):
60
+ assert apply_conversions(input_text) == expected
61
+
62
+ def test_tag_conversion(self):
63
+ assert apply_conversions("[tag]") == "<!-- tag: tag -->"
64
+ assert apply_conversions("[python]") == "<!-- tag: python -->"
65
+ assert (
66
+ apply_conversions("Text with [important] tag")
67
+ == "Text with <!-- tag: important --> tag"
68
+ )
69
+
70
+
71
+ class TestCodeBlockProcessor:
72
+ @pytest.mark.parametrize(
73
+ "input_text,expected",
74
+ [
75
+ ("code:example.py\nprint('Hello')", "```py\nprint('Hello')\n```"),
76
+ (
77
+ "code:test.js\nconsole.log('test');",
78
+ "```js\nconsole.log('test');\n```",
79
+ ),
80
+ ("code:styles.css", "```css\n```"),
81
+ ],
82
+ )
83
+ def test_basic_code_block_conversions(self, input_text, expected):
84
+ result = convert_code_blocks(input_text)
85
+ assert result == expected
86
+
87
+ def test_latex_code_block_conversion(self):
88
+ input_text = "code:tex\nE = mc^2\nV(X) = \\sigma^2"
89
+ expected = "$E = mc^2$\n$V(X) = \\sigma^2$"
90
+
91
+ assert convert_code_blocks(input_text) == expected
92
+
93
+ def test_protect_and_restore_code_blocks(self):
94
+ content = "Text before\n```python\ncode here\n```\nText after"
95
+ protected, blocks = protect_code_blocks(content)
96
+ restored = restore_code_blocks(protected, blocks)
97
+
98
+ assert restored == content
99
+
100
+
101
+ class TestListProcessor:
102
+ @pytest.mark.parametrize(
103
+ "input_text,expected",
104
+ [
105
+ (" Test", "- Test"),
106
+ (" Indented", " - Indented"),
107
+ (" Further indented", " - Further indented"),
108
+ ("\tTab indented", "- Tab indented"),
109
+ (" Full-width space", "- Full-width space"),
110
+ ],
111
+ )
112
+ def test_list_conversions(self, input_text, expected):
113
+ result = convert_lists(input_text)
114
+ assert result == expected
115
+
116
+ def test_nested_list_indentation(self):
117
+ input_text = (
118
+ " Item 1\n Sub-item 1.1\n Sub-sub-item 1.1.1\n Sub-item 1.2\n Item 2"
119
+ )
120
+ result = convert_lists(input_text)
121
+
122
+ expected_lines = [
123
+ "- Item 1",
124
+ " - Sub-item 1.1",
125
+ " - Sub-sub-item 1.1.1",
126
+ " - Sub-item 1.2",
127
+ "- Item 2",
128
+ ]
129
+ assert result == "\n".join(expected_lines)
130
+
131
+
132
+ class TestTableProcessor:
133
+ def test_basic_table_conversion(self):
134
+ input_text = "table:User Data\n Name Age City\n Alice 25 Tokyo\n Bob 30 Osaka"
135
+ expected = """## User Data
136
+
137
+ | Name | Age | City |
138
+ |---|---|---|
139
+ | Alice | 25 | Tokyo |
140
+ | Bob | 30 | Osaka |
141
+ """
142
+
143
+ assert convert_tables(input_text) == expected
144
+
145
+ def test_table_without_name(self):
146
+ input_text = "table:\n Col1 Col2\n A B"
147
+ expected = """| Col1 | Col2 |
148
+ |---|---|
149
+ | A | B |
150
+ """
151
+
152
+ assert convert_tables(input_text) == expected
153
+
154
+
155
+ class TestMarkdownConverterIntegration:
156
+ def test_mime_type_acceptance(self):
157
+ converter = MarkdownConverter()
158
+ stream = io.BytesIO(b"test")
159
+
160
+ stream_info = StreamInfo(extension=".txt")
161
+ assert converter.accepts(stream, stream_info)
162
+
163
+ stream_info = StreamInfo(extension=".pdf")
164
+ assert not converter.accepts(stream, stream_info)
165
+
166
+ def test_comprehensive_conversion(self):
167
+ content = """[* Main Heading]
168
+ [** Sub Heading]
169
+ [/ italic] and [- strikethrough]
170
+ [*/ bold italic] and [*- bold strikethrough]
171
+
172
+ Links and Images:
173
+ [Google https://google.com]
174
+ [https://example.com/image.png]
175
+ [img https://example.com/logo.png]
176
+
177
+ Lists:
178
+ Item 1
179
+ Nested 1-1
180
+ Deep nested
181
+ Item 2
182
+
183
+ Code:
184
+ code:python
185
+ def hello():
186
+ print("world")
187
+
188
+ Math:
189
+ [$ E = mc^2 $]
190
+
191
+ code:tex
192
+ V(X) = E[(X-\\mu)^2]
193
+
194
+ Table:
195
+ table:Data
196
+ Name Score
197
+ Alice 95
198
+ Bob 87"""
199
+
200
+ from markitdown_cosense._plugin import _convert_content
201
+
202
+ result = _convert_content(content)
203
+
204
+ expected = """# Main Heading
205
+ ## Sub Heading
206
+ *italic* and ~~strikethrough~~
207
+ ***bold italic*** and **~~bold strikethrough~~**
208
+
209
+ Links and Images:
210
+ [Google](https://google.com)
211
+ ![](https://example.com/image.png)
212
+ ![img](https://example.com/logo.png)
213
+
214
+ Lists:
215
+ - Item 1
216
+ - Nested 1-1
217
+ - Deep nested
218
+ - Item 2
219
+
220
+ Code:
221
+ ```python
222
+ def hello():
223
+ print("world")
224
+
225
+ Math:
226
+ [$ E = mc^2 $]
227
+
228
+ ```
229
+ $V(X) = E[(X-\\mu)^2]$
230
+
231
+ Table:
232
+ ## Data
233
+
234
+ | Name | Score |
235
+ |---|---|
236
+ | Alice | 95 |
237
+ | Bob | 87 |"""
238
+
239
+ assert result == expected
240
+
241
+
242
+ class TestPluginInterface:
243
+ def test_register_converters_success(self):
244
+ md = MarkItDown()
245
+
246
+ register_converters(md)
247
+
248
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
249
+ f.write("[* Test] and [tag]")
250
+ temp_path = f.name
251
+
252
+ try:
253
+ result = md.convert(temp_path)
254
+ expected = "# Test and <!-- tag: tag -->"
255
+ assert result.text_content == expected
256
+ finally:
257
+ os.unlink(temp_path)
258
+
259
+
260
+ class TestExceptions:
261
+ def test_pattern_compilation_error(self):
262
+ from markitdown_cosense import _plugin
263
+
264
+ original_patterns = _plugin.CONVERSION_PATTERNS
265
+ _plugin.CONVERSION_PATTERNS = [("[invalid(regex", "test")]
266
+ _plugin._compiled_patterns = None
267
+
268
+ try:
269
+ with pytest.raises(re.error):
270
+ apply_conversions("test")
271
+ finally:
272
+ _plugin.CONVERSION_PATTERNS = original_patterns
273
+ _plugin._compiled_patterns = None