markdown-flow 0.3.1__tar.gz → 0.3.2__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.
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/PKG-INFO +1 -1
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/__init__.py +1 -1
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/constants.py +12 -3
- markdown_flow-0.3.2/markdown_flow/escaping.py +141 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/interaction.py +38 -33
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow.egg-info/PKG-INFO +1 -1
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow.egg-info/SOURCES.txt +1 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/LICENSE +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/README.md +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/constants_system_prompt.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/core.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/__init__.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/classifier.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/format.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/patterns.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/sanitize.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/stream.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/formatter/types.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/higher_priority_prompt.md +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/llm.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/models.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/__init__.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/code_fence_utils.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/html_comment_utils.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/json_parser.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/output.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/preprocessor.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/validation.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/parser/variable.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/providers/__init__.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/providers/config.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/providers/openai.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/system_prompt.md +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow/tag_filter.py +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/pyproject.toml +0 -0
- {markdown_flow-0.3.1 → markdown_flow-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: An agent library designed to parse and process MarkdownFlow documents
|
|
5
5
|
Project-URL: Homepage, https://github.com/ai-shifu/markdown-flow-agent-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ai-shifu/markdown-flow-agent-py/issues
|
|
@@ -13,9 +13,18 @@ COMPILED_PERCENT_VARIABLE_REGEX = re.compile(
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
# Interaction regex base patterns
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
#
|
|
17
|
+
# The bracket content accepts an escaped delimiter as one unit, so a `\]` inside an option does
|
|
18
|
+
# not end the interaction. The three alternatives are mutually exclusive on purpose: a backslash
|
|
19
|
+
# before a delimiter can only be read as an escape, so the pattern cannot backtrack into reading
|
|
20
|
+
# it as plain text and then closing on the very bracket it escapes. Without that, this regex and
|
|
21
|
+
# a left-to-right scanner disagree about `?[a\\]`, and two parsers of one grammar must not.
|
|
22
|
+
# A backslash before anything else -- LaTeX such as `$\pi$`, or `\\` as a line break -- stays
|
|
23
|
+
# ordinary text, exactly as it was before escapes existed. See markdown_flow.escaping.
|
|
24
|
+
_INTERACTION_CONTENT = r"(?:\\[|/.\]]|\\(?![|/.\]])|[^\]\\])*"
|
|
25
|
+
INTERACTION_PATTERN = rf"(?<!\\)\?\[({_INTERACTION_CONTENT})\](?!\()" # Base pattern with capturing group for content extraction, excludes escaped \?[]
|
|
26
|
+
INTERACTION_PATTERN_NON_CAPTURING = rf"(?<!\\)\?\[{_INTERACTION_CONTENT}\](?!\()" # Non-capturing version for block splitting, excludes escaped \?[]
|
|
27
|
+
INTERACTION_PATTERN_SPLIT = rf"((?<!\\)\?\[{_INTERACTION_CONTENT}\](?!\())" # Pattern for re.split() with outer capturing group, excludes escaped \?[]
|
|
19
28
|
|
|
20
29
|
# InteractionParser specific regex patterns
|
|
21
30
|
COMPILED_INTERACTION_REGEX = re.compile(INTERACTION_PATTERN) # Main interaction pattern matcher
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Backslash escapes for the delimiters inside an interaction's brackets.
|
|
2
|
+
|
|
3
|
+
`?[A|B]` gives `|`, `//`, `...` and `]` structural meaning, so option text that contains one of
|
|
4
|
+
them is reshaped: a bar splits one choice into two, a double slash gives half the display away to
|
|
5
|
+
the stored value, an ellipsis turns the rest into a text box, and a bracket ends the interaction
|
|
6
|
+
early. An option carrying a URL or a regular expression could not be written at all.
|
|
7
|
+
|
|
8
|
+
A backslash before one of those characters now makes it ordinary text.
|
|
9
|
+
|
|
10
|
+
The escape is deliberately *selective*: a backslash is an escape only when the character after it
|
|
11
|
+
is a delimiter, and is literal text everywhere else. `\\\\` is not an escape either. That matters
|
|
12
|
+
for content that already exists: of 110,685 interactions in published courses, 97 contain a
|
|
13
|
+
backslash and every one of them is LaTeX -- `$\\pi_\\theta$`, `\\beta`, `\\nabla`, and `\\\\` as a
|
|
14
|
+
line break. A general "backslash escapes the next character" rule would render those as
|
|
15
|
+
`$pi_theta$`. Under this rule, none of them change.
|
|
16
|
+
|
|
17
|
+
The cost is that a literal backslash cannot sit immediately before a literal delimiter: `\\|` is
|
|
18
|
+
an escaped bar, not a backslash and a separator. Nothing in the measured corpus writes that.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# The characters a backslash may make ordinary. `/` covers `//`, `.` covers `...`.
|
|
22
|
+
ESCAPABLE = "|/.]"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_escape(text: str, index: int) -> bool:
|
|
26
|
+
"""Whether the backslash at `index` escapes the character after it."""
|
|
27
|
+
return index + 1 < len(text) and text[index] == "\\" and text[index + 1] in ESCAPABLE
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def escape_interaction_text(text: str) -> str:
|
|
31
|
+
"""Make `text` safe to write inside `?[...]`, keeping every character it has.
|
|
32
|
+
|
|
33
|
+
Escaping each delimiter rather than the backslashes as well is what keeps existing content
|
|
34
|
+
readable: a script that writes `$\\pi$` in an option still writes `$\\pi$` afterwards.
|
|
35
|
+
"""
|
|
36
|
+
out: list[str] = []
|
|
37
|
+
for char in text:
|
|
38
|
+
if char in ESCAPABLE:
|
|
39
|
+
out.append("\\")
|
|
40
|
+
out.append(char)
|
|
41
|
+
return "".join(out)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def unescape_interaction_text(text: str) -> str:
|
|
45
|
+
"""Resolve the escapes in one option's text, leaving every other backslash alone."""
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
index = 0
|
|
48
|
+
while index < len(text):
|
|
49
|
+
if is_escape(text, index):
|
|
50
|
+
out.append(text[index + 1])
|
|
51
|
+
index += 2
|
|
52
|
+
continue
|
|
53
|
+
out.append(text[index])
|
|
54
|
+
index += 1
|
|
55
|
+
return "".join(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_unescaped(text: str, needle: str, start: int = 0) -> int:
|
|
59
|
+
"""Index of the first `needle` that is not escaped and does not begin inside an escape.
|
|
60
|
+
|
|
61
|
+
Returns -1 when there is none. `needle` is matched whole: a `//` whose first slash is escaped
|
|
62
|
+
is not a separator even though its second slash is bare, because that second slash is the
|
|
63
|
+
escaped character of the pair.
|
|
64
|
+
"""
|
|
65
|
+
index = start
|
|
66
|
+
while index < len(text):
|
|
67
|
+
if is_escape(text, index):
|
|
68
|
+
index += 2
|
|
69
|
+
continue
|
|
70
|
+
if text.startswith(needle, index):
|
|
71
|
+
return index
|
|
72
|
+
index += 1
|
|
73
|
+
return -1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def split_unescaped(text: str, separator: str) -> list[str]:
|
|
77
|
+
"""Split on every unescaped `separator`, leaving the escapes themselves in place."""
|
|
78
|
+
parts: list[str] = []
|
|
79
|
+
start = 0
|
|
80
|
+
while True:
|
|
81
|
+
found = find_unescaped(text, separator, start)
|
|
82
|
+
if found < 0:
|
|
83
|
+
parts.append(text[start:])
|
|
84
|
+
return parts
|
|
85
|
+
parts.append(text[start:found])
|
|
86
|
+
start = found + len(separator)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def split_on_single_pipe(text: str) -> list[str]:
|
|
90
|
+
"""Split on unescaped single bars, leaving `||` joined as the multi-select separator."""
|
|
91
|
+
parts: list[str] = []
|
|
92
|
+
start = 0
|
|
93
|
+
index = 0
|
|
94
|
+
while index < len(text):
|
|
95
|
+
if is_escape(text, index):
|
|
96
|
+
index += 2
|
|
97
|
+
continue
|
|
98
|
+
if text[index] == "|":
|
|
99
|
+
if text.startswith("||", index):
|
|
100
|
+
index += 2
|
|
101
|
+
continue
|
|
102
|
+
if index > 0 and text[index - 1] == "|" and not is_escape(text, index - 1):
|
|
103
|
+
index += 1
|
|
104
|
+
continue
|
|
105
|
+
parts.append(text[start:index])
|
|
106
|
+
start = index + 1
|
|
107
|
+
index += 1
|
|
108
|
+
parts.append(text[start:])
|
|
109
|
+
return parts
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def rfind_unescaped(text: str, needle: str) -> int:
|
|
113
|
+
"""Index of the *last* unescaped `needle`, or -1."""
|
|
114
|
+
found = -1
|
|
115
|
+
start = 0
|
|
116
|
+
while True:
|
|
117
|
+
at = find_unescaped(text, needle, start)
|
|
118
|
+
if at < 0:
|
|
119
|
+
return found
|
|
120
|
+
found = at
|
|
121
|
+
start = at + 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def split_on_ellipsis(content: str) -> tuple[str, str] | None:
|
|
125
|
+
"""Split an interaction's content at the `...` that opens its text box, or None.
|
|
126
|
+
|
|
127
|
+
Two details are inherited rather than chosen, because changing either would rewrite questions
|
|
128
|
+
in courses that are already published:
|
|
129
|
+
|
|
130
|
+
* the ellipsis counts only on the first line. The regex this replaces was anchored and its
|
|
131
|
+
`.` did not cross a newline, so `A\n| ...hint` has always been read as two buttons, one of
|
|
132
|
+
them named `...hint`. 122 published interactions depend on that reading.
|
|
133
|
+
* the *last* ellipsis on that line wins, which is what the greedy group matched.
|
|
134
|
+
|
|
135
|
+
What is new is only that an escaped ellipsis no longer counts.
|
|
136
|
+
"""
|
|
137
|
+
head = content.split("\n", 1)[0]
|
|
138
|
+
at = rfind_unescaped(head, "...")
|
|
139
|
+
if at < 0:
|
|
140
|
+
return None
|
|
141
|
+
return head[:at], head[at + 3 :]
|
|
@@ -12,8 +12,13 @@ from ..constants import (
|
|
|
12
12
|
COMPILED_INTERACTION_REGEX,
|
|
13
13
|
COMPILED_LAYER1_INTERACTION_REGEX,
|
|
14
14
|
COMPILED_LAYER2_VARIABLE_REGEX,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
)
|
|
16
|
+
from ..escaping import (
|
|
17
|
+
find_unescaped,
|
|
18
|
+
split_on_ellipsis,
|
|
19
|
+
split_on_single_pipe,
|
|
20
|
+
split_unescaped,
|
|
21
|
+
unescape_interaction_text,
|
|
17
22
|
)
|
|
18
23
|
|
|
19
24
|
|
|
@@ -46,12 +51,10 @@ def extract_interaction_question(content: str) -> str | None:
|
|
|
46
51
|
# Extract interaction content (remove ?[ and ])
|
|
47
52
|
interaction_content = match.group(1) if match.groups() else match.group(0)[2:-1]
|
|
48
53
|
|
|
49
|
-
# Find ... separator, question text follows
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if len(parts) > 1:
|
|
54
|
-
return parts[1].strip()
|
|
54
|
+
# Find ... separator, question text follows. An ellipsis an option escaped is not one.
|
|
55
|
+
at = find_unescaped(interaction_content, "...")
|
|
56
|
+
if at >= 0:
|
|
57
|
+
return unescape_interaction_text(interaction_content[at + 3 :]).strip()
|
|
55
58
|
|
|
56
59
|
return None # type: ignore[unreachable]
|
|
57
60
|
|
|
@@ -153,13 +156,13 @@ class InteractionParser:
|
|
|
153
156
|
Returns:
|
|
154
157
|
Parsing result dictionary
|
|
155
158
|
"""
|
|
156
|
-
# Detect ... separator
|
|
157
|
-
|
|
159
|
+
# Detect ... separator, ignoring any an option escaped.
|
|
160
|
+
ellipsis = split_on_ellipsis(content)
|
|
158
161
|
|
|
159
|
-
if
|
|
162
|
+
if ellipsis is not None:
|
|
160
163
|
# Has ... separator
|
|
161
|
-
before_ellipsis =
|
|
162
|
-
question =
|
|
164
|
+
before_ellipsis = ellipsis[0].strip()
|
|
165
|
+
question = unescape_interaction_text(ellipsis[1]).strip()
|
|
163
166
|
|
|
164
167
|
if before_ellipsis:
|
|
165
168
|
# Has prefix content (buttons or single option) + text input
|
|
@@ -180,7 +183,7 @@ class InteractionParser:
|
|
|
180
183
|
"is_multi_select": False,
|
|
181
184
|
}
|
|
182
185
|
# No ... separator
|
|
183
|
-
if
|
|
186
|
+
if content and find_unescaped(content, "|") >= 0: # type: ignore[unreachable]
|
|
184
187
|
# Pure button group
|
|
185
188
|
buttons, is_multi_select = self._parse_buttons(content)
|
|
186
189
|
interaction_type = InteractionType.BUTTONS_MULTI_SELECT if is_multi_select else InteractionType.BUTTONS_ONLY
|
|
@@ -229,10 +232,10 @@ class InteractionParser:
|
|
|
229
232
|
"buttons": [{"display": "", "value": ""}],
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
if
|
|
234
|
-
before_ellipsis =
|
|
235
|
-
question =
|
|
235
|
+
ellipsis = split_on_ellipsis(content)
|
|
236
|
+
if ellipsis is not None:
|
|
237
|
+
before_ellipsis = ellipsis[0].strip()
|
|
238
|
+
question = unescape_interaction_text(ellipsis[1]).strip()
|
|
236
239
|
|
|
237
240
|
if before_ellipsis:
|
|
238
241
|
# Buttons + text input: ?[A | B | ...question]
|
|
@@ -251,7 +254,7 @@ class InteractionParser:
|
|
|
251
254
|
"is_multi_select": False,
|
|
252
255
|
}
|
|
253
256
|
|
|
254
|
-
if "|"
|
|
257
|
+
if find_unescaped(content, "|") >= 0: # type: ignore[unreachable]
|
|
255
258
|
# Button group: ?[A | B] or ?[A || B]
|
|
256
259
|
buttons, is_multi_select = self._parse_buttons(content)
|
|
257
260
|
return {
|
|
@@ -287,11 +290,10 @@ class InteractionParser:
|
|
|
287
290
|
# Use different splitting logic based on separator type
|
|
288
291
|
if is_multi_select:
|
|
289
292
|
# Multi-select mode: split on ||, preserve single |
|
|
290
|
-
button_parts = content
|
|
293
|
+
button_parts = split_unescaped(content, "||")
|
|
291
294
|
else:
|
|
292
295
|
# Single-select mode: split on single |, but preserve ||
|
|
293
|
-
|
|
294
|
-
button_parts = COMPILED_SINGLE_PIPE_SPLIT_REGEX.split(content)
|
|
296
|
+
button_parts = split_on_single_pipe(content)
|
|
295
297
|
|
|
296
298
|
for button_text in button_parts:
|
|
297
299
|
button_text = button_text.strip()
|
|
@@ -300,7 +302,8 @@ class InteractionParser:
|
|
|
300
302
|
buttons.append(button)
|
|
301
303
|
except (TypeError, ValueError):
|
|
302
304
|
# Fallback to treating entire content as single button
|
|
303
|
-
|
|
305
|
+
whole = unescape_interaction_text(content).strip()
|
|
306
|
+
return [{"display": whole, "value": whole}], False
|
|
304
307
|
|
|
305
308
|
# For empty content (like just separators), return empty list
|
|
306
309
|
if not buttons and (content.strip() == "||" or content.strip() == "|"):
|
|
@@ -308,7 +311,8 @@ class InteractionParser:
|
|
|
308
311
|
|
|
309
312
|
# Ensure at least one button exists (but only if there's actual content)
|
|
310
313
|
if not buttons and content.strip():
|
|
311
|
-
|
|
314
|
+
whole = unescape_interaction_text(content).strip()
|
|
315
|
+
buttons = [{"display": whole, "value": whole}]
|
|
312
316
|
|
|
313
317
|
return buttons, is_multi_select
|
|
314
318
|
|
|
@@ -330,18 +334,19 @@ class InteractionParser:
|
|
|
330
334
|
return {"display": "", "value": ""}
|
|
331
335
|
|
|
332
336
|
try:
|
|
333
|
-
# Detect Button//value format - split only on first //
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
display =
|
|
337
|
-
value = parts[1] if len(parts) > 1 else ""
|
|
337
|
+
# Detect Button//value format - split only on the first unescaped //
|
|
338
|
+
at = find_unescaped(button_text, "//")
|
|
339
|
+
if at >= 0:
|
|
340
|
+
display = unescape_interaction_text(button_text[:at]).strip()
|
|
338
341
|
# Don't strip value to preserve intentional spacing/formatting
|
|
342
|
+
value = unescape_interaction_text(button_text[at + 2 :])
|
|
339
343
|
return {"display": display, "value": value}
|
|
340
344
|
except (ValueError, IndexError):
|
|
341
345
|
# Fallback: use text as both display and value
|
|
342
346
|
pass
|
|
343
347
|
|
|
344
|
-
|
|
348
|
+
resolved = unescape_interaction_text(button_text)
|
|
349
|
+
return {"display": resolved, "value": resolved}
|
|
345
350
|
|
|
346
351
|
def _detect_separator_type(self, content: str) -> tuple[str, bool]:
|
|
347
352
|
"""
|
|
@@ -359,9 +364,9 @@ class InteractionParser:
|
|
|
359
364
|
if not content or not isinstance(content, str):
|
|
360
365
|
return "|", False
|
|
361
366
|
|
|
362
|
-
# Find first occurrence of separators
|
|
363
|
-
single_pos = content
|
|
364
|
-
double_pos = content
|
|
367
|
+
# Find first occurrence of separators. A bar an option escaped is not one.
|
|
368
|
+
single_pos = find_unescaped(content, "|")
|
|
369
|
+
double_pos = find_unescaped(content, "||")
|
|
365
370
|
|
|
366
371
|
# If no separators found
|
|
367
372
|
if single_pos == -1 and double_pos == -1:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: An agent library designed to parse and process MarkdownFlow documents
|
|
5
5
|
Project-URL: Homepage, https://github.com/ai-shifu/markdown-flow-agent-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ai-shifu/markdown-flow-agent-py/issues
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|