@braccato/core 0.1.7 → 1.0.0
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.
- package/LICENSE +1 -1
- package/README.md +267 -32
- package/dist/LICENSE +21 -0
- package/dist/README.md +298 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +42 -0
- package/dist/element.d.ts +155 -0
- package/dist/element.js +655 -0
- package/dist/engine.d.ts +309 -0
- package/dist/engine.js +1940 -0
- package/dist/index.d.ts +12 -128
- package/dist/index.js +11 -949
- package/dist/inject.d.ts +37 -0
- package/dist/inject.js +376 -0
- package/dist/instrumental.d.ts +10 -0
- package/dist/instrumental.js +78 -0
- package/dist/renderer.d.ts +29 -0
- package/dist/renderer.js +349 -0
- package/dist/seek.d.ts +1 -0
- package/dist/seek.js +27 -0
- package/dist/styles/instrumental.css +103 -0
- package/dist/styles/lyrics.css +274 -0
- package/dist/styles/variables.css +181 -0
- package/dist/text.d.ts +11 -0
- package/dist/text.js +54 -0
- package/dist/themeSettings.d.ts +23 -0
- package/dist/themeSettings.js +126 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.js +64 -0
- package/dist/view.d.ts +22 -0
- package/dist/view.js +93 -0
- package/package.json +34 -21
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# @braccato/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A custom element that renders synchronized lyrics and lights each syllable up as it is sung. No
|
|
4
|
+
runtime dependencies, only a types-only one on `@braccato/types`. The lines go into light DOM rather
|
|
5
|
+
than a shadow root, so the CSS already on your page reaches them.
|
|
6
|
+
|
|
7
|
+
Extracted from the [Better Lyrics](https://better-lyrics.boidu.dev) rendering engine, which is still
|
|
8
|
+
where it runs.
|
|
4
9
|
|
|
5
10
|
## Install
|
|
6
11
|
|
|
@@ -12,52 +17,282 @@ npm i @braccato/core
|
|
|
12
17
|
|
|
13
18
|
```html
|
|
14
19
|
<audio id="player" src="song.mp3" controls></audio>
|
|
15
|
-
<braccato-lyrics source="#player"
|
|
20
|
+
<braccato-lyrics source="#player"></braccato-lyrics>
|
|
16
21
|
|
|
17
22
|
<script type="module">
|
|
18
|
-
import "@braccato/core";
|
|
23
|
+
import "@braccato/core/element";
|
|
24
|
+
import "@braccato/core/styles/variables.css";
|
|
25
|
+
import "@braccato/core/styles/lyrics.css";
|
|
26
|
+
import "@braccato/core/styles/instrumental.css";
|
|
27
|
+
|
|
28
|
+
document.querySelector("braccato-lyrics").lyrics = [
|
|
29
|
+
{ startTimeMs: 0, durationMs: 4200, words: "The first line" },
|
|
30
|
+
{ startTimeMs: 4200, durationMs: 3800, words: "The second" },
|
|
31
|
+
];
|
|
19
32
|
</script>
|
|
20
33
|
```
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
`source` takes a CSS selector or a media element. It is resolved when the element connects, so put
|
|
36
|
+
the `<audio>` before the tag, or write the property from script. Without a source, drive the view
|
|
37
|
+
yourself by writing `currentTime` and `playing`.
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
Two things catch everybody once. The element has no `display` of its own:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
braccato-lyrics {
|
|
43
|
+
display: block;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
And autoscroll writes `scrollTop` on the nearest ancestor that scrolls, falling through to the
|
|
48
|
+
document when nothing does. If the element is not inside its own scroller, say which one it is:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
view.host = { getScrollElement: () => yourFrame };
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Lyrics
|
|
55
|
+
|
|
56
|
+
The array is the whole input, and nothing in this package produces one.
|
|
57
|
+
[`@braccato/parsers`](https://www.npmjs.com/package/@braccato/parsers) reads TTML, LRC, SRT, QRC and
|
|
58
|
+
plain text, and picks between them by looking at the file.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { detectParser } from "@braccato/parsers";
|
|
62
|
+
|
|
63
|
+
const text = await fetch("song.ttml").then(response => response.text());
|
|
64
|
+
view.lyrics = detectParser(text).parse(text, player.duration * 1000);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A `Lyric` is `{ startTimeMs, durationMs, words }`, with an optional `parts` array of the same three
|
|
68
|
+
fields for syllable or word timing, and optional `translation`, `romanization` and
|
|
69
|
+
`timedRomanization` beside them.
|
|
25
70
|
|
|
26
71
|
## Properties
|
|
27
72
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
73
|
+
Every one of these may be written before the element is in a document. The renderer is built when it
|
|
74
|
+
connects, and everything it was handed by then is applied at once.
|
|
75
|
+
|
|
76
|
+
| Property | Attribute | Type | Default | Description |
|
|
77
|
+
| --------------- | -------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `lyrics` | | `Lyric[] \| null` | `null` | The song. Null means it was never given one, and an empty array clears the view, so there is a way to say both. |
|
|
79
|
+
| `lyricsOptions` | | `{ loaderVisible?, noLyrics? }` | `{}` | How the lines are built. `noLyrics` marks a message as a placeholder rather than a song, which keeps passive scrolling from drifting it. |
|
|
80
|
+
| `source` | `source` | `string \| HTMLMediaElement \| null` | `null` | A selector or the media element itself. See Following a media element. |
|
|
81
|
+
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
|
+
| `currentTime` | `current-time` | `number` | `0` | Playback position in **seconds**. Writing it renders the view again, so whoever holds the clock drives the lyrics by writing this. |
|
|
83
|
+
| `playing` | `playing` | `boolean` | `false` | A paused view animates differently from a playing one. |
|
|
84
|
+
| `tickOptions` | | `ElementTickOptions` | `{}` | The rest of a tick: four offsets taken off the clock before it is matched, whether passive scrolling is on, and when the clock was sampled. |
|
|
85
|
+
| `theme` | `theme` | `string` | `""` | A compiled stylesheet. See Theming. |
|
|
86
|
+
| `host` | | `Partial<LyricsRendererHost>` | `{}` | Overrides for what the renderer asks of its surroundings. Every member has a default. Writing it while connected rebuilds the view. |
|
|
87
|
+
| `renderer` | | `LyricsRenderer \| null` (get) | `null` | The renderer underneath, for the day the tag runs out. A different one after every reconnection. |
|
|
88
|
+
| `status` | | `ElementStatus` (get) | `"idle"` | `idle`, `rendering`, `theme-conflict` or `no-browsing-context`. |
|
|
89
|
+
|
|
90
|
+
`tickOptions` and `lyricsOptions` are stored on write and read by the next tick or the next build, so
|
|
91
|
+
writing options and the clock on the same frame renders once.
|
|
92
|
+
|
|
93
|
+
## Attributes
|
|
94
|
+
|
|
95
|
+
An attribute writes its property, and a property never writes back. Reflecting `current-time` would
|
|
96
|
+
put the playback clock into the DOM sixty times a second, and one attribute reflecting while the rest
|
|
97
|
+
do not is worse than none of them doing it.
|
|
98
|
+
|
|
99
|
+
| Attribute | Writes | Notes |
|
|
100
|
+
| -------------- | ------------- | -------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `source` | `source` | The selector form only. Another selector moves the binding, and removing it unbinds. |
|
|
102
|
+
| `theme` | `theme` | A whole stylesheet in an attribute value. It works, but nobody would ship a theme this way. |
|
|
103
|
+
| `current-time` | `currentTime` | Seconds. A value that does not parse as a number is ignored rather than read as zero. |
|
|
104
|
+
| `playing` | `playing` | An ordinary boolean attribute: its presence is what counts, so `playing="false"` is playing. |
|
|
37
105
|
|
|
38
106
|
## Events
|
|
39
107
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
| `braccato:
|
|
46
|
-
| `braccato:
|
|
108
|
+
All four bubble and are composed, so an element you put inside your own shadow root still reaches
|
|
109
|
+
your listener.
|
|
110
|
+
|
|
111
|
+
| Event | Detail | When |
|
|
112
|
+
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------- |
|
|
113
|
+
| `braccato:lyrics-loaded` | `{ lineCount, syncType }` | Lyrics were applied, including an empty array. A theme change that rebuilds the lines reports itself the same way. |
|
|
114
|
+
| `braccato:line-click` | `{ timeS }` | A line was clicked. The seek has already reached the bound media element by the time you hear about it. |
|
|
115
|
+
| `braccato:scroll-state` | `{ userScrolling }` | Autoscroll stopped following the song, or started again. |
|
|
116
|
+
| `braccato:error` | `{ phase, error }` | Connecting, resolving a source, or applying lyrics or a theme went wrong. `phase` is `connect`, `conflict`, `source`, `lyrics` or `theme`. |
|
|
117
|
+
|
|
118
|
+
Errors are dispatched a microtask after they happen rather than where they happen, which is what
|
|
119
|
+
makes them receivable at all: `connectedCallback` runs before any listener a page could have added.
|
|
120
|
+
A listener added later than that still misses them, so `status` answers the same question and needs
|
|
121
|
+
no listener. Nothing thrown by a tick lands here, because sixty error events a second would bury the
|
|
122
|
+
one that mattered.
|
|
123
|
+
|
|
124
|
+
There is no `braccato:word-click`. The renderer tells its host `seek(timeS)` and nothing else, so the
|
|
125
|
+
element cannot tell a word seek from a line seek without re-deriving the click branch off the DOM.
|
|
126
|
+
The DOM is light and the class names are published, so listen for `click` on the element and read
|
|
127
|
+
`.blyrics--word` yourself.
|
|
128
|
+
|
|
129
|
+
## Theming
|
|
130
|
+
|
|
131
|
+
A theme is a stylesheet. Write CSS against the class names below and the module stays out of it. What
|
|
132
|
+
it does read is the `blyrics-*` lines inside the comments, which is how a theme changes behaviour
|
|
133
|
+
without a second configuration format.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
view.theme = `
|
|
137
|
+
/* blyrics-target-scroll-pos-ratio = 0.5; */
|
|
138
|
+
/* blyrics-long-word-threshold = 900; */
|
|
139
|
+
|
|
140
|
+
.blyrics-container {
|
|
141
|
+
--blyrics-font-size: 3.5rem;
|
|
142
|
+
--blyrics-lyric-active-color: white;
|
|
143
|
+
--blyrics-lyric-inactive-color: rgb(255 255 255 / 0.25);
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Settings are read from comments only. Everything else is CSS the browser is going to read, and a
|
|
149
|
+
stylesheet must not be able to configure the module by accident. An empty theme puts every setting
|
|
150
|
+
back to its default. The stylesheet itself goes into the document head under the
|
|
151
|
+
`blyrics-custom-style` id.
|
|
152
|
+
|
|
153
|
+
There is no `longWordThreshold`, `lineSyncedDelay` or `disableRichsync` property. Those are theme
|
|
154
|
+
settings (`blyrics-long-word-threshold`, `blyrics-line-synced-animation-delay`,
|
|
155
|
+
`blyrics-disable-richsync`), read from the stylesheet you already hand over. A theme that set one
|
|
156
|
+
while a property said otherwise would leave the module with two answers and no rule for picking.
|
|
157
|
+
|
|
158
|
+
`parseThemeConfig` is published on `@braccato/core/themeSettings` for reading the settings out of a
|
|
159
|
+
stylesheet somewhere no renderer is running.
|
|
160
|
+
|
|
161
|
+
### Custom properties
|
|
47
162
|
|
|
48
|
-
|
|
163
|
+
The ones a theme reaches for first. `variables.css` declares the rest.
|
|
49
164
|
|
|
50
165
|
```css
|
|
51
|
-
|
|
52
|
-
--
|
|
53
|
-
--
|
|
54
|
-
--
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--braccato-richsync-timing-offset: 0.150s;
|
|
166
|
+
.blyrics-container {
|
|
167
|
+
--blyrics-font-family: system-ui, sans-serif;
|
|
168
|
+
--blyrics-font-size: 3rem;
|
|
169
|
+
--blyrics-line-height: 1.333;
|
|
170
|
+
--blyrics-padding: 2rem;
|
|
171
|
+
--blyrics-lyric-active-color: white;
|
|
172
|
+
--blyrics-lyric-inactive-color: rgb(255 255 255 / 0.3);
|
|
173
|
+
--blyrics-glow-color: rgb(255 255 255 / 0.5);
|
|
60
174
|
}
|
|
61
175
|
```
|
|
62
176
|
|
|
63
|
-
|
|
177
|
+
`--blyrics-font-size` is what everything else is sized off, including the instrumental dots.
|
|
178
|
+
`--blyrics-padding` is the vertical room around each line, and the one to reach for before
|
|
179
|
+
`line-height`. Every word is given the glow, so a theme that wants it to mean something selects on
|
|
180
|
+
`data-long-word`, which the module sets on any part held past `blyrics-long-word-threshold`.
|
|
181
|
+
|
|
182
|
+
### Class names
|
|
183
|
+
|
|
184
|
+
These are published API rather than implementation. Renaming one costs a migration rather than a
|
|
185
|
+
refactor. Import them from `@braccato/core/constants` instead of typing them out.
|
|
186
|
+
|
|
187
|
+
| Constant | Class | What it is |
|
|
188
|
+
| ------------------------- | --------------------------- | ----------------------------------------------------------- |
|
|
189
|
+
| `LYRICS_CLASS` | `blyrics-container` | The view. One per renderer. |
|
|
190
|
+
| `LINE_CLASS` | `blyrics--line` | One line, carrying its own `dir="auto"`. |
|
|
191
|
+
| `CURRENT_LYRICS_CLASS` | `blyrics--active` | The line the song is on right now. |
|
|
192
|
+
| `WORD_CLASS` | `blyrics--word` | One word, and the unit the sweep animates. |
|
|
193
|
+
| `BACKGROUND_LYRIC_CLASS` | `blyrics-background-lyric` | A background vocal, sung over the line it answers. |
|
|
194
|
+
| `USER_SCROLLING_CLASS` | `blyrics-user-scrolling` | Set while a reader has scrolled away and autoscroll waits. |
|
|
195
|
+
| `TRANSLATED_LYRICS_CLASS` | `blyrics--translated` | A translation hung off a line that was already built. |
|
|
196
|
+
| `CUSTOM_THEME_STYLE_ID` | `blyrics-custom-style` | The id of the `<style>` the theme lands in. |
|
|
197
|
+
|
|
198
|
+
## Stylesheets
|
|
199
|
+
|
|
200
|
+
Three sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
|
|
201
|
+
them out and you get lines that are in the document and unstyled, rather than lines that are missing.
|
|
202
|
+
|
|
203
|
+
| File | What it carries |
|
|
204
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
205
|
+
| `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the other two read from it. |
|
|
206
|
+
| `@braccato/core/styles/lyrics.css` | The container, the lines, the words and the sweep, plus two `@property` registrations the word animation interpolates through. |
|
|
207
|
+
| `@braccato/core/styles/instrumental.css` | The waveform that fills a bar nobody sings over, and the animation that walks it. |
|
|
208
|
+
|
|
209
|
+
One thing they do not do for you. The module measures the room the first and last lines need to reach
|
|
210
|
+
the view's target scroll position and writes it on the root as `--blyrics-padding-top` and
|
|
211
|
+
`--blyrics-padding-bottom`, but `lyrics.css` only spends the bottom one. Supply the top rule:
|
|
212
|
+
|
|
213
|
+
```css
|
|
214
|
+
.blyrics-container {
|
|
215
|
+
padding-top: var(--blyrics-padding-top, 2rem);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Light DOM, not shadow DOM
|
|
220
|
+
|
|
221
|
+
The element builds into itself. That is what lets a stylesheet at document level select the lines,
|
|
222
|
+
and what lets the package's own `@property` registrations apply to them, which they would not inside
|
|
223
|
+
a shadow root. The theme is adopted into the element's document rather than encapsulated, and the
|
|
224
|
+
package's stylesheets are yours to load for the same reason.
|
|
225
|
+
|
|
226
|
+
## Entry points
|
|
227
|
+
|
|
228
|
+
`@braccato/core` is the facade and registers nothing. `createLyricsRenderer(options)` returns one
|
|
229
|
+
`LyricsRenderer`: give it lyrics, tick it, and it owns the DOM it builds and every re-measurement
|
|
230
|
+
that DOM needs. `resetPlaybackClock`, `resumeAllAutoscroll`, `injectRomanization` and
|
|
231
|
+
`injectTranslation` are published beside it, for what one instance cannot answer for on its own.
|
|
232
|
+
|
|
233
|
+
`@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
|
|
234
|
+
Registration is a side effect, which is why it is entered separately.
|
|
235
|
+
|
|
236
|
+
Four leaves import nothing at all, so taking one does not pull the engine into your bundle with it:
|
|
237
|
+
|
|
238
|
+
- `@braccato/core/constants` for the class names and element ids above
|
|
239
|
+
- `@braccato/core/text` for script detection: `testRtl`, `containsNonLatin`, `detectNonLatinLanguage`
|
|
240
|
+
- `@braccato/core/themeSettings` for `parseThemeConfig`
|
|
241
|
+
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`
|
|
242
|
+
|
|
243
|
+
Two notes on the element entry point. A browser extension's isolated world has no custom element
|
|
244
|
+
registry, so `window.customElements` is null there and importing this file throws where it registers.
|
|
245
|
+
An extension that wants the tag has to run in the page's own world; one that stays isolated calls
|
|
246
|
+
`createLyricsRenderer` directly. And registration is silent about a name already taken, so two copies
|
|
247
|
+
of this package on one page means the first to load takes both names and `instanceof` against the
|
|
248
|
+
second copy's class is false for every element on the page. Load one copy.
|
|
249
|
+
|
|
250
|
+
## Following a media element
|
|
251
|
+
|
|
252
|
+
While a `source` is bound, the element drives itself. It reads `currentTime` and `paused` off the
|
|
253
|
+
media element on a `requestAnimationFrame` loop that runs only while the song plays, and a click on a
|
|
254
|
+
lyric line sets `currentTime` back on it. So `currentTime` and `playing` become outputs: a write to
|
|
255
|
+
either is dropped and the getter keeps reporting what the binding last read. Dropped rather than
|
|
256
|
+
reported, because a consumer who bound a source and left their own frame loop running would otherwise
|
|
257
|
+
be told about it sixty times a second. Unbind and the clock goes back to whoever asked for it.
|
|
258
|
+
|
|
259
|
+
A reading the media element has not refreshed yet is carried forward at the playback rate it was
|
|
260
|
+
taken at, capped at 100ms of frame time. That cap is what covers a stall: the view runs at most 100ms
|
|
261
|
+
past the last real reading and then waits with it. What it costs is a step backwards when the clock
|
|
262
|
+
moves again, scaled by the rate. 100ms at 1x, 400ms at 4x.
|
|
263
|
+
|
|
264
|
+
`play`, `pause`, `seeking`, `seeked` and `ratechange` are listened to. The frame loop covers the rest
|
|
265
|
+
by asking the media element whether its clock is still going rather than trusting that something said
|
|
266
|
+
so. One gap is worth knowing: `emptied` while already paused leaves no loop running to notice, so
|
|
267
|
+
swapping `audio.src` between songs without playing goes on reporting the old position until the next
|
|
268
|
+
`play`.
|
|
269
|
+
|
|
270
|
+
## One renderer per document
|
|
271
|
+
|
|
272
|
+
Two renderers in one document write over each other, so the module supports one. It is a constraint
|
|
273
|
+
rather than a setting, and it is stated rather than enforced: none of the points where two of them
|
|
274
|
+
collide is a crash.
|
|
275
|
+
|
|
276
|
+
Two things are written per document and belong to whichever renderer wrote them last: the theme's
|
|
277
|
+
`<style>` element, and the scroll padding on the root. Two more are per bundle, because a settings
|
|
278
|
+
registry and the playback clock both live at module scope: one theme means one set of values for
|
|
279
|
+
every view in that bundle, and whichever view ticked last is the one whose clock the others replay.
|
|
280
|
+
|
|
281
|
+
None of that is a limit on how many elements you may have. Two views handed the **same** theme share
|
|
282
|
+
only the settings both of them asked for, and the renderer adopts an existing theme element rather
|
|
283
|
+
than adding a rival under the same id. The line is drawn at the disagreement: when an element applies
|
|
284
|
+
a theme another element in its document was not given, both dispatch `braccato:error` with
|
|
285
|
+
`phase: "conflict"` and both read `status === "theme-conflict"`. Neither stops rendering, because a
|
|
286
|
+
blank view with a reason is worse than a themed one with a warning.
|
|
287
|
+
|
|
288
|
+
## Docs and demo
|
|
289
|
+
|
|
290
|
+
Full documentation is at [braccato.boidu.dev](https://braccato.boidu.dev).
|
|
291
|
+
|
|
292
|
+
The demo page lives at [`demo/`](https://github.com/better-lyrics/braccato/tree/master/demo) in the
|
|
293
|
+
repository and runs against the emitted package, with a control for most of what is above. Clone the
|
|
294
|
+
repository and run `pnpm -C demo dev`, then open `http://localhost:5173/`.
|
|
295
|
+
|
|
296
|
+
## Licence
|
|
297
|
+
|
|
298
|
+
MIT. See `LICENSE`.
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Boidushya Bhattacharya
|
|
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.
|