qparse 0.1.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.
qparse/qmd_unpacker.py ADDED
@@ -0,0 +1,169 @@
1
+ # -*- coding: utf-8 -*-
2
+ """将 .qmd 文件解包为可编辑的工作目录。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ import shutil
9
+ import zipfile
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+ from uuid import uuid4
13
+
14
+
15
+ class QmdUnpacker:
16
+ """
17
+ 解压 .qmd 包:
18
+ 1. 展开 documents / resources / themes / templates
19
+ 2. 按 manifest 将资源复制到对应 Markdown 同级的 assets/
20
+ 3. 返回解包目录与入口 render.md 路径
21
+ """
22
+
23
+ IMAGE_PATTERN = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)")
24
+ HTML_IMG_PATTERN = re.compile(
25
+ r'<img\b[^>]*\bsrc\s*=\s*["\']([^"\']+)["\'][^>]*>',
26
+ re.IGNORECASE,
27
+ )
28
+
29
+ def __init__(self, package: str):
30
+ self.package_path = Path(package).resolve()
31
+ if not self.package_path.exists():
32
+ raise FileNotFoundError(f"qmd 包不存在: {self.package_path}")
33
+ if not zipfile.is_zipfile(self.package_path):
34
+ raise ValueError(f"不是有效的 qmd 压缩包: {self.package_path}")
35
+
36
+ self.manifest: dict = {}
37
+ self.target_dir: Optional[Path] = None
38
+
39
+ def unpack(self, target: Optional[str] = None) -> Path:
40
+ """
41
+ 解包到目标目录。
42
+
43
+ :param target: 目标目录。为空时创建到包旁的 .qmd_runtime/<uuid>
44
+ :return: 解包后的根目录
45
+ """
46
+ if target:
47
+ self.target_dir = Path(target).resolve()
48
+ else:
49
+ self.target_dir = (
50
+ self.package_path.parent
51
+ / ".qmd_runtime"
52
+ / uuid4().hex
53
+ )
54
+
55
+ if self.target_dir.exists():
56
+ shutil.rmtree(self.target_dir)
57
+ self.target_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ with zipfile.ZipFile(self.package_path, "r") as zf:
60
+ zf.extractall(self.target_dir)
61
+
62
+ manifest_path = self.target_dir / "manifest.json"
63
+ if not manifest_path.exists():
64
+ raise FileNotFoundError(
65
+ f"缺少 manifest.json: {manifest_path}"
66
+ )
67
+ self.manifest = json.loads(
68
+ manifest_path.read_text(encoding="utf-8")
69
+ )
70
+
71
+ self._materialize_document_assets()
72
+ return self.target_dir
73
+
74
+ @property
75
+ def entry_path(self) -> Path:
76
+ if self.target_dir is None:
77
+ raise RuntimeError("请先调用 unpack()")
78
+ entry = self.manifest.get("entry", "render.md")
79
+ path = self.target_dir / entry
80
+ if not path.exists():
81
+ raise FileNotFoundError(f"入口文件不存在: {path}")
82
+ return path
83
+
84
+ @property
85
+ def resources_dir(self) -> Path:
86
+ if self.target_dir is None:
87
+ raise RuntimeError("请先调用 unpack()")
88
+ return self.target_dir / "resources"
89
+
90
+ def get_resource_path(self, sha256: str) -> Optional[Path]:
91
+ resources = self.manifest.get("resources", {})
92
+ item = resources.get(sha256)
93
+ if not item:
94
+ return None
95
+ path = self.target_dir / item["path"]
96
+ return path if path.exists() else None
97
+
98
+ def _materialize_document_assets(self) -> None:
99
+ """
100
+ 按需将 resources 中的文件复制到文档同级 assets/,
101
+ 方便编辑器直接预览图片。
102
+ """
103
+ document_assets: Dict[str, List[str]] = self.manifest.get(
104
+ "document_assets",
105
+ {},
106
+ )
107
+ resources: Dict[str, dict] = self.manifest.get("resources", {})
108
+
109
+ if document_assets:
110
+ for doc_rel, sha_list in document_assets.items():
111
+ doc_path = self.target_dir / doc_rel
112
+ if not doc_path.exists():
113
+ continue
114
+ assets_dir = doc_path.parent / "assets"
115
+ assets_dir.mkdir(parents=True, exist_ok=True)
116
+ for sha in sha_list:
117
+ item = resources.get(sha)
118
+ if not item:
119
+ continue
120
+ source = self.target_dir / item["path"]
121
+ if not source.exists():
122
+ continue
123
+ target = assets_dir / Path(item["path"]).name
124
+ if not target.exists():
125
+ shutil.copy2(source, target)
126
+ return
127
+
128
+ # 兼容没有 document_assets 字段的旧包:扫描 Markdown 中的 ./assets/ 引用
129
+ for md_path in self.target_dir.rglob("*.md"):
130
+ text = md_path.read_text(encoding="utf-8")
131
+ names = set()
132
+ for match in self.IMAGE_PATTERN.finditer(text):
133
+ url = match.group(2).strip().split(" ", 1)[0]
134
+ name = self._asset_name_from_url(url)
135
+ if name:
136
+ names.add(name)
137
+ for match in self.HTML_IMG_PATTERN.finditer(text):
138
+ name = self._asset_name_from_url(match.group(1))
139
+ if name:
140
+ names.add(name)
141
+ if not names:
142
+ continue
143
+
144
+ assets_dir = md_path.parent / "assets"
145
+ assets_dir.mkdir(parents=True, exist_ok=True)
146
+ for name in names:
147
+ source = self.resources_dir / name
148
+ if not source.exists():
149
+ # 也可能在 templates/
150
+ alt = self.target_dir / "templates" / name
151
+ source = alt if alt.exists() else source
152
+ if source.exists():
153
+ target = assets_dir / name
154
+ if not target.exists():
155
+ shutil.copy2(source, target)
156
+
157
+ @staticmethod
158
+ def _asset_name_from_url(url: str) -> Optional[str]:
159
+ value = url.strip().strip('"').strip("'")
160
+ if not value:
161
+ return None
162
+ if value.startswith("http://") or value.startswith("https://"):
163
+ return None
164
+ path = Path(value.replace("\\", "/"))
165
+ if path.parts and path.parts[0] in {".", "..", "assets"}:
166
+ return path.name
167
+ if "assets/" in value.replace("\\", "/"):
168
+ return path.name
169
+ return None
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+ import json
4
+ from .default_render_option import get_default_render_option,SafeGetRenderOption,default_referance_path
5
+ from jinja2 import Environment
6
+ from pathlib import Path
7
+ # 全局复用jinja环境,不要每次新建
8
+ _qeditor_jinja_env = Environment(
9
+ variable_start_string="[%",
10
+ variable_end_string="%]",
11
+ block_start_string="{%",
12
+ block_end_string="%}",
13
+ comment_start_string="{#",
14
+ comment_end_string="#}",
15
+ autoescape=True,
16
+ )
17
+
18
+ def render_template(template_text: str, context: dict) -> str:
19
+ """
20
+ 模板渲染,占位符:[% var_name %]
21
+ :param template_text: 原始模板字符串,包含 [% label %] [% text %]
22
+ :param context: 字典 {"label":"xxx","text":"yyy"}
23
+ :return: 渲染完成后的字符串
24
+ """
25
+ tpl = _qeditor_jinja_env.from_string(template_text)
26
+ return tpl.render(**context)
27
+
28
+ def get_default_theme():
29
+ fdir = Path(__file__).parent/"theme.json"
30
+ with open(fdir,'r',encoding='utf-8') as fp:
31
+ themes = json.load(fp=fp)
32
+ for theme_key,option in themes.items():
33
+
34
+ option['docx'].update({"referance":default_referance_path.as_posix()})
35
+ return themes
36
+
37
+
38
+ DEFAULT_RENDER_OPTION = get_default_render_option()
39
+
40
+
@@ -0,0 +1,86 @@
1
+ # -*- coding: utf-8 -*-
2
+ import copy
3
+ from typing import List
4
+ from pathlib import Path
5
+ default_referance_path = Path(__file__).parent / "referance.docx"
6
+
7
+ def SafeGetRenderOption(option:dict,path:List[str],default=None):
8
+ node_option = option
9
+ for p in path:
10
+ if node_option:= node_option.get(p,None):
11
+ pass
12
+ else:
13
+ return default
14
+ return node_option
15
+
16
+ def get_default_render_option():
17
+ default_render_option = {
18
+ "docx":{
19
+ 'referance':default_referance_path.as_posix(),
20
+ "markdwon-table": {
21
+ "width_percent": 100,
22
+ "style": "Table Grid",
23
+ "header_bold": True,
24
+ "header_shading": "D9EAF7",
25
+ "border_color": "B7C9D6",
26
+ "cell_alignment": "center",
27
+ }
28
+ },
29
+ "markdown": {
30
+ "head": {
31
+ "downgrade_heading_level": True,
32
+ }
33
+ },
34
+ "question": {
35
+ "show_answer": False,
36
+ "stem": {
37
+ "show": True,
38
+ "keep_together":True,
39
+ "::first-text-visible": {
40
+ "template": "[%question_id%]、[%text%]",
41
+ '[answer_types="multiple-choice"]':{
42
+ "template": "[%question_id%](多选)、[%text%]"
43
+ }
44
+ },
45
+ '[answer_types="multiple-choice"]':{
46
+ "template": "[%text%]"
47
+ },
48
+ '[answer_types="choice"]':{
49
+ "template": "[%text%]"
50
+ },
51
+ '[answer_types="blank"]':{
52
+ "template": "[%text%]\n\n\n\n"
53
+ },
54
+ '[answer_types="answer"]':{
55
+ "template": "[%text%]\n\n\n\n"
56
+ },
57
+ },
58
+ "analysis": {
59
+ "show": False,
60
+ "::first-text-visible": {
61
+ "template": "[% text %]"
62
+ },
63
+ },
64
+ ".answer": {
65
+ ":show_answer": {"template": "[%text%]"},
66
+ ":not_show_answer": {"template": ""},
67
+ },
68
+ ".blank": {
69
+ ":show_answer": {"template": "[%text%]"},
70
+ ":not_show_answer": {"template": " $\\underline{\\hspace{1.8cm}}$ "},
71
+ },
72
+ ".choise": {
73
+ ":show_answer": {"template": "( [% text %] )"},
74
+ ":not_show_answer": {"template": "( )"},
75
+ },
76
+
77
+ ".multiple-choice": {
78
+ ":show_answer": {"template": "([%text%])"},
79
+ ":not_show_answer": {"template": "( )"},
80
+ },
81
+
82
+ },
83
+ }
84
+ return copy.deepcopy(default_render_option)
85
+
86
+
@@ -0,0 +1,21 @@
1
+ <params>
2
+ <var name="markdwon_src" value="[%markdwon_src%]" type="str">
3
+ <!--markdwon 主文件入口-->
4
+ </var>
5
+ <var name="docx_referance" value="[%docx_referance%]" type="str">
6
+ <!--markdwon docx模板样式-->
7
+ </var>
8
+ <var name="render_options" value="[%render_options%]" type="str">
9
+ <!--渲染主题,一般用json 格式存储-->
10
+ </var>
11
+
12
+ </params>
13
+ <system-params>
14
+ <!--markdwon 系统变量方便取用-->
15
+ <var name="docDir" desc="markdwon_src所在目录">
16
+ <var name="docStem" desc="markdwon_src所在目录的文件名">
17
+ </system-params>
18
+
19
+ <render to="[%docDir%]\[%docStem%]">
20
+
21
+
Binary file
@@ -0,0 +1,245 @@
1
+ {
2
+ "Worksheet": {
3
+ "docx": {
4
+ "referance": "./referance.docx",
5
+ "markdwon-table": {
6
+ "width_percent": 100,
7
+ "style": "Table Grid",
8
+ "header_bold": true,
9
+ "header_shading": "D9EAF7",
10
+ "border_color": "B7C9D6",
11
+ "cell_alignment": "center"
12
+ }
13
+ },
14
+ "markdown": {
15
+ "head": {
16
+ "downgrade_heading_level": true
17
+ }
18
+ },
19
+ "question": {
20
+ "show_answer": false,
21
+ "stem": {
22
+ "show": true,
23
+ "keep_together": true,
24
+ "::first-text-visible": {
25
+ "template": "[%question_id%]、[%text%]",
26
+ "[answer_types=\"multiple-choice\"]": {
27
+ "template": "[%question_id%](多选)、[%text%]"
28
+ }
29
+ },
30
+ "[answer_types=\"multiple-choice\"]": {
31
+ "template": "[%text%]"
32
+ },
33
+ "[answer_types=\"choice\"]": {
34
+ "template": "[%text%]"
35
+ },
36
+ "[answer_types=\"blank\"]": {
37
+ "template": "[%text%]\n\n\n\n"
38
+ },
39
+ "[answer_types=\"answer\"]": {
40
+ "template": "[%text%]\n\n\n\n"
41
+ }
42
+ },
43
+ "analysis": {
44
+ "show": false,
45
+ "::first-text-visible": {
46
+ "template": "[% text %]"
47
+ }
48
+ },
49
+ ".answer": {
50
+ ":show_answer": {
51
+ "template": "[%text%]"
52
+ },
53
+ ":not_show_answer": {
54
+ "template": ""
55
+ }
56
+ },
57
+ ".blank": {
58
+ ":show_answer": {
59
+ "template": "[%text%]"
60
+ },
61
+ ":not_show_answer": {
62
+ "template": " $\\underline{\\hspace{1.8cm}}$ "
63
+ }
64
+ },
65
+ ".choise": {
66
+ ":show_answer": {
67
+ "template": "( [% text %] )"
68
+ },
69
+ ":not_show_answer": {
70
+ "template": "( )"
71
+ }
72
+ },
73
+ ".multiple-choice": {
74
+ ":show_answer": {
75
+ "template": "([%text%])"
76
+ },
77
+ ":not_show_answer": {
78
+ "template": "( )"
79
+ }
80
+ }
81
+ }
82
+ },
83
+ "SolutionSheet": {
84
+ "docx": {
85
+ "referance": "./referance.docx",
86
+ "markdwon-table": {
87
+ "width_percent": 100,
88
+ "style": "Table Grid",
89
+ "header_bold": true,
90
+ "header_shading": "D9EAF7",
91
+ "border_color": "B7C9D6",
92
+ "cell_alignment": "center"
93
+ }
94
+ },
95
+ "markdown": {
96
+ "head": {
97
+ "downgrade_heading_level": true
98
+ }
99
+ },
100
+ "question": {
101
+ "show_answer": false,
102
+ "stem": {
103
+ "show": false,
104
+ "keep_together": true,
105
+ "::first-text-visible": {
106
+ "template": "[%question_id%]、[%text%]",
107
+ "[answer_types=\"multiple-choice\"]": {
108
+ "template": "[%question_id%](多选)、[%text%]"
109
+ }
110
+ },
111
+ "[answer_types=\"multiple-choice\"]": {
112
+ "template": "[%text%]"
113
+ },
114
+ "[answer_types=\"choice\"]": {
115
+ "template": "[%text%]"
116
+ },
117
+ "[answer_types=\"blank\"]": {
118
+ "template": "[%text%]\n\n\n\n"
119
+ },
120
+ "[answer_types=\"answer\"]": {
121
+ "template": "[%text%]\n\n\n\n"
122
+ }
123
+ },
124
+ "analysis": {
125
+ "show": true,
126
+ "::first-text-visible": {
127
+ "template": "[% text %]"
128
+ }
129
+ },
130
+ ".answer": {
131
+ ":show_answer": {
132
+ "template": "[%text%]"
133
+ },
134
+ ":not_show_answer": {
135
+ "template": ""
136
+ }
137
+ },
138
+ ".blank": {
139
+ ":show_answer": {
140
+ "template": "[%text%]"
141
+ },
142
+ ":not_show_answer": {
143
+ "template": " $\\underline{\\hspace{1.8cm}}$ "
144
+ }
145
+ },
146
+ ".choise": {
147
+ ":show_answer": {
148
+ "template": "( [% text %] )"
149
+ },
150
+ ":not_show_answer": {
151
+ "template": "( )"
152
+ }
153
+ },
154
+ ".multiple-choice": {
155
+ ":show_answer": {
156
+ "template": "([%text%])"
157
+ },
158
+ ":not_show_answer": {
159
+ "template": "( )"
160
+ }
161
+ }
162
+ }
163
+ },
164
+ "AnnotatedWorksheet": {
165
+ "docx": {
166
+ "referance": "./referance.docx",
167
+ "markdwon-table": {
168
+ "width_percent": 100,
169
+ "style": "Table Grid",
170
+ "header_bold": true,
171
+ "header_shading": "D9EAF7",
172
+ "border_color": "B7C9D6",
173
+ "cell_alignment": "center"
174
+ }
175
+ },
176
+ "markdown": {
177
+ "head": {
178
+ "downgrade_heading_level": true
179
+ }
180
+ },
181
+ "question": {
182
+ "show_answer": true,
183
+ "stem": {
184
+ "show": true,
185
+ "keep_together": true,
186
+ "::first-text-visible": {
187
+ "template": "[%question_id%]、[%text%]",
188
+ "[answer_types=\"multiple-choice\"]": {
189
+ "template": "[%question_id%](多选)、[%text%]"
190
+ }
191
+ },
192
+ "[answer_types=\"multiple-choice\"]": {
193
+ "template": "[%text%]"
194
+ },
195
+ "[answer_types=\"choice\"]": {
196
+ "template": "[%text%]"
197
+ },
198
+ "[answer_types=\"blank\"]": {
199
+ "template": "[%text%]\n\n\n\n"
200
+ },
201
+ "[answer_types=\"answer\"]": {
202
+ "template": "[%text%]\n\n\n\n"
203
+ }
204
+ },
205
+ "analysis": {
206
+ "show": true,
207
+ "::first-text-visible": {
208
+ "template": "[% text %]"
209
+ }
210
+ },
211
+ ".answer": {
212
+ ":show_answer": {
213
+ "template": "[%text%]"
214
+ },
215
+ ":not_show_answer": {
216
+ "template": ""
217
+ }
218
+ },
219
+ ".blank": {
220
+ ":show_answer": {
221
+ "template": "[%text%]"
222
+ },
223
+ ":not_show_answer": {
224
+ "template": " $\\underline{\\hspace{1.8cm}}$ "
225
+ }
226
+ },
227
+ ".choise": {
228
+ ":show_answer": {
229
+ "template": "( [% text %] )"
230
+ },
231
+ ":not_show_answer": {
232
+ "template": "( )"
233
+ }
234
+ },
235
+ ".multiple-choice": {
236
+ ":show_answer": {
237
+ "template": "([%text%])"
238
+ },
239
+ ":not_show_answer": {
240
+ "template": "( )"
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .html_full_protector import HtmlFullProtector
3
+