markwright 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.
markwright/twitter.py ADDED
@@ -0,0 +1,187 @@
1
+ # ABOUTME: Twitter embed extension for Python-Markdown.
2
+ # Converts [twitter URL flags...] syntax to Twitter blockquote embeds with script injection.
3
+
4
+ from __future__ import annotations
5
+
6
+ import html
7
+ import re
8
+
9
+ from markdown import Markdown
10
+ from markdown.extensions import Extension
11
+ from markdown.postprocessors import Postprocessor
12
+ from markdown.preprocessors import Preprocessor
13
+
14
+ from markwright._util import URL_SCHEME_WWW_PREFIX
15
+
16
+ # Scheme, "www.", and the twitter.com/x.com host are all optional, and when
17
+ # present must be followed by a "/", so inputs from a bare "user/status/id"
18
+ # up to a full "https://www.twitter.com/user/status/id" all match. Ported
19
+ # verbatim from upstream twitter.js's URL-matching grammar.
20
+ TWITTER_RE = re.compile(
21
+ r"^\[twitter\s+(?:(?:" + URL_SCHEME_WWW_PREFIX + r"(?:twitter|x)\.com)?/)?(\w+)/status/(\d+)"
22
+ r"((?:\s+(?:light|dark|left|center|right|\d+))*)\]$"
23
+ )
24
+
25
+ TWITTER_MIN_WIDTH = 250
26
+ TWITTER_MAX_WIDTH = 550
27
+ TWITTER_DEFAULT_WIDTH = 550
28
+
29
+ TWITTER_SIGNATURE = 'class="twitter-tweet"'
30
+
31
+ TWITTER_SCRIPT = '<script async defer src="https://platform.twitter.com/widgets.js"></script>'
32
+
33
+
34
+ def _parse_twitter_flags(raw_flags: str) -> dict[str, str | int]:
35
+ """Parse space-separated Twitter flags into a settings dictionary.
36
+
37
+ :param raw_flags: Raw space-separated flags string.
38
+ :returns: Dictionary with theme, alignment, and width settings.
39
+ """
40
+ flags = raw_flags.split()
41
+
42
+ theme = "dark" if "dark" in flags else "light"
43
+
44
+ alignment = "center"
45
+ for align_option in ("left", "right", "center"):
46
+ if align_option in flags:
47
+ alignment = align_option
48
+ break
49
+
50
+ width = TWITTER_DEFAULT_WIDTH
51
+ for flag in flags:
52
+ if flag.isdigit():
53
+ width = max(TWITTER_MIN_WIDTH, min(TWITTER_MAX_WIDTH, int(flag)))
54
+ break
55
+
56
+ return {
57
+ "theme": theme,
58
+ "alignment": alignment,
59
+ "width": width,
60
+ }
61
+
62
+
63
+ def _render_match(line: str) -> str | None:
64
+ """Build the Twitter embed HTML for a standalone embed line.
65
+
66
+ :param line: A single source line.
67
+ :returns: The embed HTML if the line is a Twitter embed, else ``None``.
68
+ """
69
+ twitter_match = TWITTER_RE.match(line.strip())
70
+ if not twitter_match:
71
+ return None
72
+
73
+ user = twitter_match.group(1)
74
+ status_id = twitter_match.group(2)
75
+ raw_flags = twitter_match.group(3).strip()
76
+ settings = _parse_twitter_flags(raw_flags)
77
+
78
+ # Canonicalize to twitter.com
79
+ canonical_url = f"https://twitter.com/{user}/status/{status_id}"
80
+ escaped_url = html.escape(canonical_url)
81
+ escaped_user = html.escape(user)
82
+
83
+ theme = settings["theme"]
84
+ alignment = settings["alignment"]
85
+ width = settings["width"]
86
+
87
+ align_attr = f' align="{alignment}"' if alignment != "center" else ""
88
+
89
+ return (
90
+ f'<div class="twitter"{align_attr}>\n'
91
+ f' <blockquote class="twitter-tweet" data-dnt="true"'
92
+ f' data-width="{width}" data-theme="{theme}">\n'
93
+ f' <a href="{escaped_url}">View tweet by @{escaped_user}</a>\n'
94
+ f" </blockquote>\n"
95
+ f"</div>"
96
+ )
97
+
98
+
99
+ def expand_source(text: str) -> str:
100
+ """Expand standalone Twitter embed lines to embed HTML in raw source.
101
+
102
+ Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
103
+ HTML inline without any Python-Markdown stash placeholder.
104
+
105
+ :param text: The source text.
106
+ :returns: The text with standalone Twitter embeds replaced by HTML.
107
+ """
108
+ return "\n".join(_render_match(line) or line for line in text.split("\n"))
109
+
110
+
111
+ def apply_html(rendered_html: str, warnings: list[str] | None = None) -> str:
112
+ """Inject the Twitter widgets script once if a Twitter embed is present.
113
+
114
+ Used by the ``mw post`` CLI stage and the in-process postprocessor. The
115
+ script is appended only when the Twitter class signature is present and the
116
+ script is not already injected, so the transform is idempotent.
117
+
118
+ :param rendered_html: Rendered HTML content.
119
+ :param warnings: Optional warnings list; unused (signature injection never warns).
120
+ :returns: HTML with the Twitter script appended if needed.
121
+ """
122
+ if TWITTER_SIGNATURE in rendered_html and TWITTER_SCRIPT not in rendered_html:
123
+ return rendered_html + "\n" + TWITTER_SCRIPT
124
+ return rendered_html
125
+
126
+
127
+ class TwitterPreprocessor(Preprocessor):
128
+ """Replace [twitter ...] lines with Twitter embed HTML.
129
+
130
+ :param md: The Markdown instance.
131
+ """
132
+
133
+ def run(self, lines: list[str]) -> list[str]:
134
+ """Process lines, replacing Twitter embed syntax with HTML.
135
+
136
+ :param lines: Source lines to process.
137
+ :returns: Modified lines with Twitter embeds replaced by HTML.
138
+ """
139
+ output: list[str] = []
140
+ for line in lines:
141
+ embed_html = _render_match(line)
142
+ if embed_html is not None:
143
+ output.append(self.md.htmlStash.store(embed_html))
144
+ else:
145
+ output.append(line)
146
+ return output
147
+
148
+
149
+ class TwitterPostprocessor(Postprocessor):
150
+ """Append the Twitter widgets script tag once if any embeds are present.
151
+
152
+ :param md: The Markdown instance.
153
+ """
154
+
155
+ def run(self, text: str) -> str:
156
+ """Append the script tag if a Twitter embed signature is present.
157
+
158
+ :param text: Rendered HTML content.
159
+ :returns: HTML with Twitter script appended if needed.
160
+ """
161
+ return apply_html(text)
162
+
163
+
164
+ class TwitterExtension(Extension):
165
+ """Python-Markdown extension for Twitter embeds.
166
+
167
+ :param \\*\\*kwargs: Configuration options passed to the extension.
168
+ """
169
+
170
+ def extendMarkdown(self, md: Markdown) -> None:
171
+ """Register the Twitter preprocessor and postprocessor.
172
+
173
+ :param md: The Markdown instance to extend.
174
+ """
175
+ preprocessor = TwitterPreprocessor(md)
176
+ postprocessor = TwitterPostprocessor(md)
177
+ md.preprocessors.register(preprocessor, "do-twitter", 20)
178
+ md.postprocessors.register(postprocessor, "do-twitter-script", 15)
179
+
180
+
181
+ def makeExtension(**kwargs: object) -> TwitterExtension:
182
+ """Create and return the TwitterExtension instance.
183
+
184
+ :param \\*\\*kwargs: Configuration options.
185
+ :returns: A configured TwitterExtension.
186
+ """
187
+ return TwitterExtension(**kwargs)
markwright/youtube.py ADDED
@@ -0,0 +1,106 @@
1
+ # ABOUTME: YouTube embed extension for Python-Markdown.
2
+ # Converts [youtube ID height width] syntax to responsive iframe embeds.
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import urllib.parse
8
+
9
+ from markdown import Markdown
10
+ from markdown.extensions import Extension
11
+ from markdown.preprocessors import Preprocessor
12
+
13
+ from markwright._util import reduce_fraction
14
+
15
+ YOUTUBE_RE = re.compile(r"^\[youtube (\S+?)(?:\s+(-?\d+))?(?:\s+(-?\d+))?\]$")
16
+
17
+ DEFAULT_HEIGHT = 270
18
+ DEFAULT_WIDTH = 480
19
+
20
+
21
+ def _render_match(line: str) -> str | None:
22
+ """Build the iframe HTML for a standalone YouTube embed line.
23
+
24
+ :param line: A single source line.
25
+ :returns: The iframe HTML if the line is a YouTube embed, else ``None``.
26
+ """
27
+ youtube_match = YOUTUBE_RE.match(line.strip())
28
+ if not youtube_match:
29
+ return None
30
+
31
+ video_id = youtube_match.group(1)
32
+ height = int(youtube_match.group(2)) if youtube_match.group(2) else DEFAULT_HEIGHT
33
+ width = int(youtube_match.group(3)) if youtube_match.group(3) else DEFAULT_WIDTH
34
+ if height <= 0 or width <= 0:
35
+ height, width = DEFAULT_HEIGHT, DEFAULT_WIDTH
36
+
37
+ encoded_id = urllib.parse.quote(video_id, safe="")
38
+ aspect_width, aspect_height = reduce_fraction(width, height)
39
+ aspect_ratio = f"{aspect_width}/{aspect_height}"
40
+
41
+ return (
42
+ f'<iframe src="https://www.youtube.com/embed/{encoded_id}"'
43
+ f' class="youtube" height="{height}" width="{width}"'
44
+ f' style="aspect-ratio: {aspect_ratio}" frameborder="0" allowfullscreen>\n'
45
+ f' <a href="https://www.youtube.com/watch?v={encoded_id}"'
46
+ f' target="_blank">View YouTube video</a>\n'
47
+ f"</iframe>"
48
+ )
49
+
50
+
51
+ def expand_source(text: str) -> str:
52
+ """Expand standalone YouTube embed lines to iframe HTML in raw source.
53
+
54
+ Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
55
+ HTML inline without any Python-Markdown stash placeholder.
56
+
57
+ :param text: The source text.
58
+ :returns: The text with standalone YouTube embeds replaced by iframe HTML.
59
+ """
60
+ return "\n".join(_render_match(line) or line for line in text.split("\n"))
61
+
62
+
63
+ class YouTubePreprocessor(Preprocessor):
64
+ """Replace [youtube ...] lines with responsive iframe HTML.
65
+
66
+ :param md: The Markdown instance.
67
+ """
68
+
69
+ def run(self, lines: list[str]) -> list[str]:
70
+ """Process lines, replacing YouTube embed syntax with iframe HTML.
71
+
72
+ :param lines: Source lines to process.
73
+ :returns: Modified lines with YouTube embeds replaced by HTML.
74
+ """
75
+ output: list[str] = []
76
+ for line in lines:
77
+ iframe_html = _render_match(line)
78
+ if iframe_html is not None:
79
+ output.append(self.md.htmlStash.store(iframe_html))
80
+ else:
81
+ output.append(line)
82
+ return output
83
+
84
+
85
+ class YouTubeExtension(Extension):
86
+ """Python-Markdown extension for YouTube video embeds.
87
+
88
+ :param \\*\\*kwargs: Configuration options passed to the extension.
89
+ """
90
+
91
+ def extendMarkdown(self, md: Markdown) -> None:
92
+ """Register the YouTube preprocessor.
93
+
94
+ :param md: The Markdown instance to extend.
95
+ """
96
+ preprocessor = YouTubePreprocessor(md)
97
+ md.preprocessors.register(preprocessor, "do-youtube", 20)
98
+
99
+
100
+ def makeExtension(**kwargs: object) -> YouTubeExtension:
101
+ """Create and return the YouTubeExtension instance.
102
+
103
+ :param \\*\\*kwargs: Configuration options.
104
+ :returns: A configured YouTubeExtension.
105
+ """
106
+ return YouTubeExtension(**kwargs)
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: markwright
3
+ Version: 0.1.0
4
+ Summary: Python-Markdown extensions ported from DigitalOcean's do-markdownit
5
+ Keywords: markdown,python-markdown,do-markdownit,mkdocs,hugo
6
+ Author: Mason Egger
7
+ Author-email: Mason Egger <mason@masonegger.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: LICENSE-Apache-2.0
11
+ License-File: NOTICE
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
19
+ Requires-Dist: markdown>=3.4
20
+ Requires-Dist: pymdown-extensions>=10.5
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://github.com/MasonEgger/markwright
23
+ Project-URL: Repository, https://github.com/MasonEgger/markwright
24
+ Project-URL: Documentation, https://masonegger.github.io/markwright/
25
+ Description-Content-Type: text/markdown
26
+
27
+ # markwright
28
+
29
+ A Python port of DigitalOcean's [`do-markdownit`](https://github.com/digitalocean/do-markdownit) JavaScript library.
30
+
31
+ These are [Python-Markdown](https://python-markdown.github.io/) extensions that bring DigitalOcean's markdown syntax (highlighted text, labeled code fences, and a set of media embeds) to any tool built on Python-Markdown, including [MkDocs](https://www.mkdocs.org/), Flask, or a plain script.
32
+ The port maintains compatibility with the original `do-markdownit` HTML output, so rendered pages match what DigitalOcean produces.
33
+ A bundled command-line tool, `mw`, runs the same extensions as pre and post filters around any renderer, so the syntax also works in toolchains that are not built on Python-Markdown, like Hugo.
34
+
35
+ Full documentation and a live demo: https://masonegger.github.io/markwright/
36
+
37
+ ## Features
38
+
39
+ Each extension is standalone and can be loaded on its own or alongside the others.
40
+
41
+ - **Highlight** wraps `<^>text<^>` in `<mark>`, and works inside inline code and fenced code blocks where `==text==` cannot reach.
42
+ - **Fence** adds labels, secondary labels, environment classes, and command-line prefixes (line numbers, `$`/`#` prompts, custom prefixes) to code blocks.
43
+ - **YouTube** turns `[youtube ID]` into a responsive iframe embed.
44
+ - **CodePen** embeds a pen with theme, default-tab, and layout options.
45
+ - **Twitter** embeds a tweet with theme and alignment options.
46
+ - **Instagram** embeds a post with caption and alignment options.
47
+ - **Slideshow** builds an image slideshow with navigation arrows.
48
+ - **Image Compare** renders two images with a draggable before/after slider.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ uv add markwright
54
+ ```
55
+
56
+ or
57
+
58
+ ```bash
59
+ pip install markwright
60
+ ```
61
+
62
+ To work against a local checkout, install it editable:
63
+
64
+ ```bash
65
+ uv add --editable ../markwright
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### With MkDocs
71
+
72
+ Add the extensions to `mkdocs.yml`.
73
+ Load `markwright.fence` after `pymdownx.superfences` and `pymdownx.highlight` so the code-block HTML is in the shape the fence postprocessor expects.
74
+
75
+ ```yaml
76
+ markdown_extensions:
77
+ - pymdownx.superfences
78
+ - pymdownx.highlight:
79
+ pygments_lang_class: true
80
+ - markwright.highlight
81
+ - markwright.fence:
82
+ allowed_environments:
83
+ - local
84
+ - second
85
+ - third
86
+ - markwright.youtube
87
+ - markwright.codepen
88
+ - markwright.twitter
89
+ - markwright.instagram
90
+ - markwright.slideshow
91
+ - markwright.image_compare
92
+ ```
93
+
94
+ ### Standalone
95
+
96
+ The extensions work with Python-Markdown directly, so you can use them in any Python program.
97
+
98
+ ```python
99
+ import markdown
100
+
101
+ md = markdown.Markdown(extensions=[
102
+ "pymdownx.superfences",
103
+ "pymdownx.highlight",
104
+ "markwright.highlight",
105
+ "markwright.fence",
106
+ "markwright.youtube",
107
+ "markwright.codepen",
108
+ "markwright.twitter",
109
+ "markwright.instagram",
110
+ "markwright.slideshow",
111
+ "markwright.image_compare",
112
+ ])
113
+
114
+ html = md.convert("This has a <^>highlighted word<^> in it.")
115
+ print(html)
116
+ ```
117
+
118
+ Load only what you need:
119
+
120
+ ```python
121
+ import markdown
122
+
123
+ # Just YouTube embeds
124
+ md = markdown.Markdown(extensions=["markwright.youtube"])
125
+ html = md.convert("[youtube dQw4w9WgXcQ]")
126
+ ```
127
+
128
+ The fence extension expects `pymdownx.superfences` and `pymdownx.highlight` to be loaded too.
129
+ Without them the preprocessor still extracts directives, but the code-block HTML may not match what the postprocessor rewrites.
130
+
131
+ ## Command-Line Interface (`mw`)
132
+
133
+ For a toolchain that is not built on Python-Markdown, the `mw` command runs the same extensions as a pair of Unix filters around any renderer: a pre stage on the Markdown source, and a post stage on the rendered HTML.
134
+
135
+ ```bash
136
+ mw pre < in.md | your-renderer | mw post > out.html
137
+ ```
138
+
139
+ - `mw pre` expands the embeds and extracts fence directives into an `mw-fence` comment.
140
+ - `mw post` applies the fence styling, resolves the highlights, and injects each embed script once.
141
+ - `mw render` runs the full pipeline in one shot for callers without their own renderer.
142
+ - `mw list` prints every extension and the stages it provides.
143
+
144
+ Your renderer must pass raw HTML and HTML comments through.
145
+ Hugo is a worked, tested example: see the [Hugo guide](https://masonegger.github.io/markwright/integrations/hugo/).
146
+ The full command and flag reference is in the [CLI docs](https://masonegger.github.io/markwright/cli/).
147
+
148
+ ## Syntax at a Glance
149
+
150
+ ````text
151
+ Highlight: <^>important<^>
152
+
153
+ Fenced code with a label and command prompts:
154
+ ```command
155
+ [label deploy.sh]
156
+ ssh root@server
157
+ apt update
158
+ ```
159
+
160
+ YouTube: [youtube dQw4w9WgXcQ]
161
+ [youtube dQw4w9WgXcQ 360 640]
162
+ CodePen: [codepen MattCowley vwPzeX]
163
+ [codepen MattCowley vwPzeX dark css]
164
+ Twitter: [twitter https://twitter.com/github/status/1234567890]
165
+ Instagram: [instagram https://www.instagram.com/p/CkQuv3_LRgS]
166
+ Slideshow: [slideshow https://picsum.photos/id/10/480/270 https://picsum.photos/id/20/480/270]
167
+ Image Compare: [compare before.jpg after.jpg]
168
+ ````
169
+
170
+ See the [extension docs](https://masonegger.github.io/markwright/) for every flag and the exact HTML each one produces.
171
+
172
+ ## Development
173
+
174
+ This project uses [`uv`](https://docs.astral.sh/uv/) and [`just`](https://github.com/casey/just).
175
+
176
+ ```bash
177
+ just install # uv sync
178
+ just test # unit tests, 100% line + branch coverage
179
+ just test-integration # end-to-end Hugo pipeline test (needs hugo)
180
+ just lint # ruff check + ruff format --check
181
+ just typecheck # mypy --strict
182
+ just check # test + lint + typecheck
183
+ just docs-serve # serve the docs site at localhost:8000
184
+ ```
185
+
186
+ `just check` must pass before a change is complete.
187
+ The tooling bar is strict: `mypy --strict`, a clean `ruff`, and full test coverage.
188
+
189
+ ## License
190
+
191
+ markwright is released under the [MIT License](LICENSE).
192
+
193
+ It is a port of [`do-markdownit`](https://github.com/digitalocean/do-markdownit) by DigitalOcean, which is licensed under the Apache License 2.0.
194
+ See the [NOTICE](NOTICE) file for attribution details.
@@ -0,0 +1,20 @@
1
+ markwright/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ markwright/_util.py,sha256=BI3Ams_h52bPjUnDWicSeacXULmxpNZmQIDj9st9ZQU,1279
3
+ markwright/cli.py,sha256=u-xX6_hiVh_NjNyuEhBiH8b0uftw4gRynluA_lOY4Fo,5565
4
+ markwright/codepen.py,sha256=R4Wo8y2znGUZuREUo-VZVIxJ2xePk30vqi1vs6lV92c,6815
5
+ markwright/fence.py,sha256=MQVYAFM-alFIJAEnRflyxs5HeKFoV73ZHrDIIzrUuF4,19768
6
+ markwright/highlight.py,sha256=Bzgz8-RFmUQ2EIEwJiDQ9n80yzRqwMZNqHh_DnhWzbs,7648
7
+ markwright/image_compare.py,sha256=EID0oa8T-uNs35KBq5WwKhS640GJmUs3AM2CTLM9Xzo,4810
8
+ markwright/instagram.py,sha256=2rJwUpqejJdOoXopLvCTATiSOXrFDPbPGaX_qIF11ZU,6644
9
+ markwright/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ markwright/registry.py,sha256=6meSIct5DdkVV_t3cX0waYit_YmJ-IRh2jJiOIgaLKc,5935
11
+ markwright/slideshow.py,sha256=rv25PkhoYTADLuIAJ_UWU3bMm1_kFrQermAbPxI7iWg,4841
12
+ markwright/twitter.py,sha256=t8yzuF5rr_mz4fDmDok0v2FMlFb6_yeocR1eNbwM8qc,6261
13
+ markwright/youtube.py,sha256=-GaDUZneA1Jji66yZvc1HmK58VhHwBxv_O00Xrkd8i0,3535
14
+ markwright-0.1.0.dist-info/licenses/LICENSE,sha256=2lCfnjZ3jo5wTPiXG_2XesGsPbfJyhAYlCd3kTmYCio,1068
15
+ markwright-0.1.0.dist-info/licenses/LICENSE-Apache-2.0,sha256=DlD_JqT3u3kroA5EzJNq_Bl5b7hSImEASqHAVdBIckg,10760
16
+ markwright-0.1.0.dist-info/licenses/NOTICE,sha256=iANynyRpH3OWLtlIHOFDUxn304VgEobUKKexJy4kYBs,1204
17
+ markwright-0.1.0.dist-info/WHEEL,sha256=_d8F1e7SqtoW6CDj4Gi8lFC26a_7I17R7zPLCKTp4Fg,81
18
+ markwright-0.1.0.dist-info/entry_points.txt,sha256=b2GIDio68udBvjKs86sxzirrQlwaCxPm6jWAk7yMMR0,44
19
+ markwright-0.1.0.dist-info/METADATA,sha256=9QXUMFUPnBINSRsF-KlX8cvxLZ2Ajpoh1wtwFMzeuRs,7058
20
+ markwright-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.15
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mw = markwright.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mason Egger
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.