markup-plus 0.6.0__py3-none-any.whl
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.
- markup_plus/__init__.py +23 -0
- markup_plus/__main__.py +8 -0
- markup_plus/ast.py +260 -0
- markup_plus/cli/__init__.py +173 -0
- markup_plus/cli/__main__.py +8 -0
- markup_plus/cli/commands.py +323 -0
- markup_plus/cli/display.py +170 -0
- markup_plus/cli/errors.py +136 -0
- markup_plus/cli/theme.py +154 -0
- markup_plus/errors.py +40 -0
- markup_plus/lexer.py +35 -0
- markup_plus/parser.py +1068 -0
- markup_plus/renderer.py +1902 -0
- markup_plus/themes/__init__.py +79 -0
- markup_plus/themes/css/base.css +1043 -0
- markup_plus-0.6.0.dist-info/METADATA +248 -0
- markup_plus-0.6.0.dist-info/RECORD +20 -0
- markup_plus-0.6.0.dist-info/WHEEL +4 -0
- markup_plus-0.6.0.dist-info/entry_points.txt +2 -0
- markup_plus-0.6.0.dist-info/licenses/LICENSE +249 -0
markup_plus/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markup+ — A modern markup language with more features than Markdown.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Markdown-compatible syntax
|
|
6
|
+
- Variables, conditionals, loops
|
|
7
|
+
- Reusable components
|
|
8
|
+
- Charts (bar, line, pie, doughnut)
|
|
9
|
+
- Math formulas (KaTeX)
|
|
10
|
+
- Tabs, collapse, alerts, timeline
|
|
11
|
+
- Auto RTL detection (Persian, Arabic, Hebrew)
|
|
12
|
+
- Rich code blocks with copy/download/preview
|
|
13
|
+
- Light/dark themes
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.6.0"
|
|
17
|
+
__author__ = "Hossein Ataee"
|
|
18
|
+
__license__ = "Markup+ — Non-Commercial License"
|
|
19
|
+
|
|
20
|
+
from .renderer import to_html
|
|
21
|
+
from .parser import parse_text
|
|
22
|
+
|
|
23
|
+
__all__ = ["to_html", "parse_text", "__version__"]
|
markup_plus/__main__.py
ADDED
markup_plus/ast.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markup+ Abstract Syntax Tree (AST)
|
|
3
|
+
|
|
4
|
+
Defines node types that represent the structure of a Markup+ document.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Dict, Optional, Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ============================================================
|
|
12
|
+
# Base
|
|
13
|
+
# ============================================================
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Node:
|
|
17
|
+
"""Base class for all AST nodes."""
|
|
18
|
+
line: int = 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# Block-level nodes
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Document(Node):
|
|
27
|
+
"""Root node — contains all blocks + metadata + variables."""
|
|
28
|
+
children: List[Node] = field(default_factory=list)
|
|
29
|
+
meta: Dict[str, str] = field(default_factory=dict)
|
|
30
|
+
footnotes: Dict[str, str] = field(default_factory=dict)
|
|
31
|
+
variables: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class VariableDef(Node):
|
|
36
|
+
"""@let name = value"""
|
|
37
|
+
name: str = ""
|
|
38
|
+
value: Any = None
|
|
39
|
+
raw_value: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Heading(Node):
|
|
44
|
+
"""# Heading 1, ## Heading 2, ### Heading 3"""
|
|
45
|
+
level: int = 1
|
|
46
|
+
text: str = ""
|
|
47
|
+
slug: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Paragraph(Node):
|
|
52
|
+
"""Regular paragraph."""
|
|
53
|
+
text: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ListBlock(Node):
|
|
58
|
+
"""Ordered or unordered list. `checked` for task lists."""
|
|
59
|
+
ordered: bool = False
|
|
60
|
+
items: List[str] = field(default_factory=list)
|
|
61
|
+
checked: List = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class BlockQuote(Node):
|
|
66
|
+
"""> Quoted text (can span multiple lines)"""
|
|
67
|
+
text: str = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class HorizontalRule(Node):
|
|
72
|
+
"""--- separator"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CodeBlock(Node):
|
|
78
|
+
"""Fenced code block with advanced options."""
|
|
79
|
+
language: str = ""
|
|
80
|
+
code: str = ""
|
|
81
|
+
title: str = ""
|
|
82
|
+
copy: bool = True
|
|
83
|
+
download: bool = True
|
|
84
|
+
run: bool = False
|
|
85
|
+
share: bool = False
|
|
86
|
+
linenos: bool = False
|
|
87
|
+
highlight: List[int] = field(default_factory=list)
|
|
88
|
+
wrap: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class ImageBlock(Node):
|
|
93
|
+
"""{options}"""
|
|
94
|
+
alt: str = ""
|
|
95
|
+
url: str = ""
|
|
96
|
+
title: str = ""
|
|
97
|
+
width: str = ""
|
|
98
|
+
height: str = ""
|
|
99
|
+
align: str = ""
|
|
100
|
+
link: str = ""
|
|
101
|
+
caption: str = ""
|
|
102
|
+
description: str = ""
|
|
103
|
+
zoomable: bool = True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class GalleryBlock(Node):
|
|
108
|
+
"""@gallery {columns=N} ... @end"""
|
|
109
|
+
columns: int = 3
|
|
110
|
+
images: List[ImageBlock] = field(default_factory=list)
|
|
111
|
+
caption: str = ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class TableBlock(Node):
|
|
116
|
+
"""| Header | Header |"""
|
|
117
|
+
headers: List[str] = field(default_factory=list)
|
|
118
|
+
rows: List[List[str]] = field(default_factory=list)
|
|
119
|
+
alignments: List[str] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class TOCBlock(Node):
|
|
124
|
+
"""@toc {title="..."}"""
|
|
125
|
+
title: str = "Table of Contents"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ============================================================
|
|
129
|
+
# Control flow nodes (Phase 4)
|
|
130
|
+
# ============================================================
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class IfBlock(Node):
|
|
134
|
+
"""@if ... @elif ... @else ... @endif"""
|
|
135
|
+
branches: List = field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class EachBlock(Node):
|
|
140
|
+
"""@each item in items ... @end"""
|
|
141
|
+
item_name: str = ""
|
|
142
|
+
index_name: str = ""
|
|
143
|
+
iterable_expr: str = ""
|
|
144
|
+
children: List[Node] = field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ImportBlock(Node):
|
|
149
|
+
"""@import "path/to/file.mup" — reserved"""
|
|
150
|
+
path: str = ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ============================================================
|
|
154
|
+
# Rich features (Phase 5)
|
|
155
|
+
# ============================================================
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class ComponentDef(Node):
|
|
159
|
+
"""@def Name(param1, param2) ... @end"""
|
|
160
|
+
name: str = ""
|
|
161
|
+
params: List[str] = field(default_factory=list)
|
|
162
|
+
children: List[Node] = field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class ComponentCall(Node):
|
|
167
|
+
"""@Name(param1="value", param2="value")"""
|
|
168
|
+
name: str = ""
|
|
169
|
+
args: Dict[str, Any] = field(default_factory=dict)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class ChartBlock(Node):
|
|
174
|
+
"""@chart(type="bar") data: [...] labels: [...] @end"""
|
|
175
|
+
chart_type: str = "bar"
|
|
176
|
+
data: List = field(default_factory=list)
|
|
177
|
+
labels: List[str] = field(default_factory=list)
|
|
178
|
+
title: str = ""
|
|
179
|
+
color: str = ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class MathBlock(Node):
|
|
184
|
+
"""$$ E = mc^2 $$"""
|
|
185
|
+
latex: str = ""
|
|
186
|
+
display: bool = True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class TabsBlock(Node):
|
|
191
|
+
"""@tabs @tab "Title" ... @end @end"""
|
|
192
|
+
tabs: List = field(default_factory=list)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class CollapseBlock(Node):
|
|
197
|
+
"""@collapse @item "Title" ... @end @end"""
|
|
198
|
+
items: List = field(default_factory=list)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class AlertBlock(Node):
|
|
203
|
+
"""@note / @warning / @tip / @danger / @success ... @end"""
|
|
204
|
+
alert_type: str = "note"
|
|
205
|
+
children: List[Node] = field(default_factory=list)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class QuoteBlock(Node):
|
|
210
|
+
"""@quote(author="...", source="...") Text... @end"""
|
|
211
|
+
author: str = ""
|
|
212
|
+
source: str = ""
|
|
213
|
+
text: str = ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass
|
|
217
|
+
class TimelineBlock(Node):
|
|
218
|
+
"""@timeline date: text @end"""
|
|
219
|
+
events: List = field(default_factory=list)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ============================================================
|
|
223
|
+
# Inline-level nodes
|
|
224
|
+
# ============================================================
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class Text(Node):
|
|
228
|
+
content: str = ""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class Bold(Node):
|
|
233
|
+
content: str = ""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class Italic(Node):
|
|
238
|
+
content: str = ""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class InlineCode(Node):
|
|
243
|
+
content: str = ""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class Strikethrough(Node):
|
|
248
|
+
content: str = ""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class Link(Node):
|
|
253
|
+
text: str = ""
|
|
254
|
+
url: str = ""
|
|
255
|
+
title: str = ""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class AutoLink(Node):
|
|
260
|
+
url: str = ""
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markup+ CLI — Command-line interface.
|
|
3
|
+
|
|
4
|
+
Provides the `mup` command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import theme
|
|
10
|
+
from .. import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
HELP_TEXT = """
|
|
14
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
15
|
+
║ Markup+ ║
|
|
16
|
+
║ A modern markup language for everyone ║
|
|
17
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
18
|
+
|
|
19
|
+
USAGE
|
|
20
|
+
mup <command> [options]
|
|
21
|
+
|
|
22
|
+
COMMANDS
|
|
23
|
+
mup <file> Convert file to HTML
|
|
24
|
+
mup <file>.md Convert Markdown to HTML
|
|
25
|
+
mup new <name> Create a new .mup file
|
|
26
|
+
mup open <file> View file in terminal
|
|
27
|
+
mup check <file> Validate file for errors
|
|
28
|
+
mup init [name] Create a new project
|
|
29
|
+
|
|
30
|
+
OPTIONS
|
|
31
|
+
-d, --dark Use dark theme
|
|
32
|
+
-l, --light Use light theme (default)
|
|
33
|
+
-r, --rtl Right-to-left layout
|
|
34
|
+
--ltr Left-to-right layout
|
|
35
|
+
-o, --output <file> Output file path
|
|
36
|
+
--css <file> Apply custom CSS file
|
|
37
|
+
--debug Show debug info
|
|
38
|
+
-h, --help Show this help
|
|
39
|
+
-v, --version Show version
|
|
40
|
+
|
|
41
|
+
EXAMPLES
|
|
42
|
+
mup hello.mup Convert hello.mup
|
|
43
|
+
mup readme.md Convert markdown to HTML
|
|
44
|
+
mup hello.mup --dark Dark theme
|
|
45
|
+
mup hello.mup --css theme.css Custom CSS theme
|
|
46
|
+
mup hello.mup --debug Show debug info
|
|
47
|
+
mup new my-doc Create my-doc.mup
|
|
48
|
+
mup open hello.mup View in terminal
|
|
49
|
+
mup check hello.mup Validate file
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> int:
|
|
54
|
+
"""Main CLI entry point."""
|
|
55
|
+
args = sys.argv[1:]
|
|
56
|
+
|
|
57
|
+
# No arguments — show help
|
|
58
|
+
if not args:
|
|
59
|
+
print(HELP_TEXT)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
# Parse global flags
|
|
63
|
+
show_help = False
|
|
64
|
+
show_version = False
|
|
65
|
+
dark = False
|
|
66
|
+
light = False
|
|
67
|
+
rtl = False
|
|
68
|
+
ltr = False
|
|
69
|
+
show_debug = False
|
|
70
|
+
output = None
|
|
71
|
+
css_file = None
|
|
72
|
+
|
|
73
|
+
remaining = []
|
|
74
|
+
i = 0
|
|
75
|
+
while i < len(args):
|
|
76
|
+
arg = args[i]
|
|
77
|
+
|
|
78
|
+
if arg in ("-h", "--help"):
|
|
79
|
+
show_help = True
|
|
80
|
+
elif arg in ("-v", "--version"):
|
|
81
|
+
show_version = True
|
|
82
|
+
elif arg in ("-d", "--dark"):
|
|
83
|
+
dark = True
|
|
84
|
+
elif arg in ("-l", "--light"):
|
|
85
|
+
light = True
|
|
86
|
+
elif arg in ("-r", "--rtl"):
|
|
87
|
+
rtl = True
|
|
88
|
+
elif arg == "--ltr":
|
|
89
|
+
ltr = True
|
|
90
|
+
elif arg == "--debug":
|
|
91
|
+
show_debug = True
|
|
92
|
+
elif arg == "--css":
|
|
93
|
+
if i + 1 < len(args):
|
|
94
|
+
css_file = args[i + 1]
|
|
95
|
+
i += 1
|
|
96
|
+
else:
|
|
97
|
+
print(theme.error("Error: --css requires a file path"))
|
|
98
|
+
return 1
|
|
99
|
+
elif arg in ("-o", "--output"):
|
|
100
|
+
if i + 1 < len(args):
|
|
101
|
+
output = args[i + 1]
|
|
102
|
+
i += 1
|
|
103
|
+
else:
|
|
104
|
+
print(theme.error("Error: --output requires a value"))
|
|
105
|
+
return 1
|
|
106
|
+
else:
|
|
107
|
+
remaining.append(arg)
|
|
108
|
+
|
|
109
|
+
i += 1
|
|
110
|
+
|
|
111
|
+
# Handle --help / --version
|
|
112
|
+
if show_help:
|
|
113
|
+
print(HELP_TEXT)
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
if show_version:
|
|
117
|
+
print(f"Markup+ v{__version__}")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
# Determine theme
|
|
121
|
+
theme_name = "dark" if dark else "light"
|
|
122
|
+
|
|
123
|
+
# Determine direction
|
|
124
|
+
direction = None
|
|
125
|
+
if rtl:
|
|
126
|
+
direction = "rtl"
|
|
127
|
+
elif ltr:
|
|
128
|
+
direction = "ltr"
|
|
129
|
+
|
|
130
|
+
# No command
|
|
131
|
+
if not remaining:
|
|
132
|
+
print(HELP_TEXT)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
# Dispatch commands
|
|
136
|
+
command = remaining[0]
|
|
137
|
+
command_args = remaining[1:]
|
|
138
|
+
|
|
139
|
+
from . import commands
|
|
140
|
+
|
|
141
|
+
if command == "new":
|
|
142
|
+
if not command_args:
|
|
143
|
+
print(theme.error("Error: 'new' requires a name"))
|
|
144
|
+
print(theme.dim("Usage: mup new <name>"))
|
|
145
|
+
return 1
|
|
146
|
+
return commands.new_file(command_args[0])
|
|
147
|
+
|
|
148
|
+
if command == "open":
|
|
149
|
+
if not command_args:
|
|
150
|
+
print(theme.error("Error: 'open' requires a file"))
|
|
151
|
+
print(theme.dim("Usage: mup open <file>"))
|
|
152
|
+
return 1
|
|
153
|
+
return commands.open_file(command_args[0])
|
|
154
|
+
|
|
155
|
+
if command == "check":
|
|
156
|
+
if not command_args:
|
|
157
|
+
print(theme.error("Error: 'check' requires a file"))
|
|
158
|
+
print(theme.dim("Usage: mup check <file>"))
|
|
159
|
+
return 1
|
|
160
|
+
return commands.check_file(command_args[0])
|
|
161
|
+
if command == "init":
|
|
162
|
+
name = command_args[0] if command_args else None
|
|
163
|
+
return commands.init_project(name)
|
|
164
|
+
|
|
165
|
+
# Default: treat as file path
|
|
166
|
+
return commands.build_file(
|
|
167
|
+
command,
|
|
168
|
+
theme_name=theme_name,
|
|
169
|
+
direction=direction,
|
|
170
|
+
output=output,
|
|
171
|
+
show_debug=show_debug,
|
|
172
|
+
css_file=css_file,
|
|
173
|
+
)
|