- /vendor/libarchive/LICENSE - /vendor/libarchive/worker-bundle.js - /vendor/libarchive/wasm-gen/libarchive.js - /vendor/libarchive/wasm-gen/libarchive.wasm - /sw.js - /offline.md - /_layouts/default.html - /_tools/serv00_post-receive - /_tools/envs_post-receive - /_data/proxylist.yml - /assets/js/offline-page.js - /assets/js/offline-unpack.jsmaster
| @ -0,0 +1,119 @@ | |||
| /* | |||
| * Mayx的博客 · /offline.html 控制页控制器(classic script,PJAX 兼容) | |||
| * | |||
| * 本体是 IIFE,不含模块语法:PJAX 换页时 jQuery 会重新执行本文件, | |||
| * 每次执行都重新查询元素、重新绑定事件,天然幂等。真正的解包逻辑在 | |||
| * ES module /assets/js/offline-unpack.js 中,通过动态 import() 加载 | |||
| * (浏览器模块缓存保证 PJAX 反复进入本页也不会重复加载)。 | |||
| * 注意:本文件经 Liquid 原样输出,不能使用模板语法字符。 | |||
| */ | |||
| (function () { | |||
| var MODULE_URL = '/assets/js/offline-unpack.js'; | |||
| // 压缩包大致量级(当前约 27MB),仅用于进度条比例,不影响正确性 | |||
| var EXPECTED_BYTES = 27 * 1024 * 1024; | |||
| var busy = false; | |||
| function el(id) { return document.getElementById(id); } | |||
| function fmtMB(bytes) { return (bytes / 1048576).toFixed(1) + ' MB'; } | |||
| function setMessage(text) { | |||
| el('offline-message').textContent = text || ''; | |||
| } | |||
| function showProgress(show) { | |||
| el('offline-progress-box').hidden = !show; | |||
| } | |||
| function render(meta) { | |||
| var online = !meta; | |||
| el('offline-state').textContent = online | |||
| ? '在线模式(离线未启用)' | |||
| : '离线模式(优先从本地缓存加载)'; | |||
| el('offline-version').textContent = online ? '-' : fmtMB(meta.payloadSize) + '(压缩包大小即版本号)'; | |||
| el('offline-files').textContent = online ? '-' : meta.files + ' 个文件 / ' + fmtMB(meta.bytes); | |||
| el('offline-time').textContent = online ? '-' : new Date(meta.unpackedAt).toLocaleString(); | |||
| el('offline-persist').textContent = online ? '-' : (meta.persisted ? '已授权(缓存不会被自动回收)' : '未授权(浏览器空间紧张时可能回收缓存)'); | |||
| el('offline-enable').disabled = busy || !online; | |||
| el('offline-disable').disabled = busy || online; | |||
| } | |||
| function onProgress(info) { | |||
| if (info.done) return; | |||
| el('offline-progress').value = Math.min(100, (info.bytes / EXPECTED_BYTES) * 100); | |||
| el('offline-progress-text').textContent = | |||
| '已解包 ' + info.files + ' 个文件(' + fmtMB(info.bytes) + ')…'; | |||
| } | |||
| function withModule(fn) { | |||
| return import(MODULE_URL).then(fn); | |||
| } | |||
| function refresh() { | |||
| if (!('serviceWorker' in navigator) || !('caches' in window)) { | |||
| el('offline-state').textContent = '当前浏览器不支持离线模式(需要 Service Worker)'; | |||
| el('offline-enable').disabled = true; | |||
| el('offline-disable').disabled = true; | |||
| return; | |||
| } | |||
| withModule(function (mod) { return mod.ensureFresh(); }).then(function (fresh) { | |||
| render(fresh.ready ? fresh.meta : null); | |||
| if (fresh.stale) { | |||
| setMessage('检测到站点压缩包已更新,离线缓存已自动失效,当前处于在线模式。可重新启用离线模式。'); | |||
| } | |||
| }).catch(function (e) { | |||
| setMessage('状态检测失败:' + (e && e.message ? e.message : e)); | |||
| }); | |||
| } | |||
| function onEnable() { | |||
| busy = true; | |||
| render(null); | |||
| showProgress(true); | |||
| setMessage('正在下载并解包站点压缩包,请保持网络连接…'); | |||
| withModule(function (mod) { return mod.activateOffline({ onProgress: onProgress }); }) | |||
| .then(function (meta) { | |||
| el('offline-progress').value = 100; | |||
| el('offline-progress-text').textContent = '解包完成'; | |||
| setMessage('离线模式已启用。之后访问本博客将优先使用本地缓存,断网也能阅读。'); | |||
| busy = false; | |||
| showProgress(false); | |||
| render(meta); // 立即展示离线状态,不必等后台刷新 | |||
| }) | |||
| .catch(function (e) { | |||
| setMessage('启用失败:' + (e && e.message ? e.message : e)); | |||
| busy = false; | |||
| showProgress(false); | |||
| refresh(); | |||
| }); | |||
| } | |||
| function onDisable() { | |||
| busy = true; | |||
| setMessage(''); | |||
| withModule(function (mod) { return mod.deactivateOffline(); }) | |||
| .then(function () { | |||
| setMessage('已切回在线模式,之后页面将通过网络加载。'); | |||
| busy = false; | |||
| render(null); | |||
| if (navigator.onLine) { | |||
| setTimeout(function () { location.reload(); }, 800); | |||
| } | |||
| }) | |||
| .catch(function (e) { | |||
| setMessage('操作失败:' + (e && e.message ? e.message : e)); | |||
| busy = false; | |||
| refresh(); | |||
| }); | |||
| } | |||
| function bind() { | |||
| var enable = el('offline-enable'); | |||
| if (!enable) return; // 不在控制页(PJAX 容错) | |||
| enable.addEventListener('click', onEnable); | |||
| el('offline-disable').addEventListener('click', onDisable); | |||
| refresh(); | |||
| } | |||
| bind(); | |||
| })(); | |||
| @ -0,0 +1,269 @@ | |||
| /* | |||
| * Mayx的博客 · 离线模式核心逻辑(/offline.html 控制页使用,ES module) | |||
| * | |||
| * - activateOffline():注册 SW,下载站点压缩包 /MayxBlog.7z,经 libarchive(WASM) | |||
| * 流式解包,把每个文件按站点路径写入 Cache Storage,最后写入就绪标记 | |||
| * (清单 + payloadSize 版本号),自此进入离线模式。 | |||
| * - ensureFresh():HEAD 探测线上压缩包大小,与标记中的 payloadSize 不一致 | |||
| * 说明站点已更新 → 清空缓存,回退在线模式。 | |||
| * - deactivateOffline():清空缓存,手动切回在线模式。 | |||
| * - readStatus():读取当前离线状态,供控制页展示。 | |||
| * | |||
| */ | |||
| const ARCHIVE_URL = '/MayxBlog.7z'; | |||
| const ARCHIVE_ROOT = 'blog/'; | |||
| const CACHE_NAME = 'mayxblog-offline-v1'; | |||
| const READY_KEY = '/__offline_manifest__'; | |||
| const WORKER_URL = '/vendor/libarchive/worker-bundle.js'; | |||
| const MIME = { | |||
| html: 'text/html; charset=utf-8', | |||
| htm: 'text/html; charset=utf-8', | |||
| css: 'text/css; charset=utf-8', | |||
| js: 'application/javascript; charset=utf-8', | |||
| mjs: 'application/javascript; charset=utf-8', | |||
| json: 'application/json; charset=utf-8', | |||
| xml: 'application/xml; charset=utf-8', | |||
| xsl: 'text/xsl; charset=utf-8', | |||
| opml: 'text/x-opml; charset=utf-8', | |||
| txt: 'text/plain; charset=utf-8', | |||
| md: 'text/markdown; charset=utf-8', | |||
| csv: 'text/csv; charset=utf-8', | |||
| svg: 'image/svg+xml', | |||
| png: 'image/png', | |||
| jpg: 'image/jpeg', | |||
| jpeg: 'image/jpeg', | |||
| gif: 'image/gif', | |||
| webp: 'image/webp', | |||
| avif: 'image/avif', | |||
| ico: 'image/x-icon', | |||
| bmp: 'image/bmp', | |||
| woff: 'font/woff', | |||
| woff2: 'font/woff2', | |||
| ttf: 'font/ttf', | |||
| otf: 'font/otf', | |||
| eot: 'application/vnd.ms-fontobject', | |||
| mp3: 'audio/mpeg', | |||
| mp4: 'video/mp4', | |||
| webm: 'video/webm', | |||
| '7z': 'application/x-7z-compressed', | |||
| zip: 'application/zip', | |||
| wasm: 'application/wasm', | |||
| mtn: 'application/octet-stream', | |||
| moc: 'application/octet-stream', | |||
| pdf: 'application/pdf', | |||
| }; | |||
| function mimeFor(pathname) { | |||
| const dot = pathname.lastIndexOf('.'); | |||
| const ext = dot === -1 ? '' : pathname.slice(dot + 1).toLowerCase(); | |||
| return MIME[ext] || 'application/octet-stream'; | |||
| } | |||
| function sitePathFor(entryPath) { | |||
| const normalized = entryPath.replace(/\\/g, '/'); | |||
| if (!normalized.startsWith(ARCHIVE_ROOT)) return null; | |||
| const relative = normalized.slice(ARCHIVE_ROOT.length); | |||
| if (!relative) return null; | |||
| const segments = []; | |||
| for (const segment of relative.split('/')) { | |||
| if (!segment || segment === '.') continue; | |||
| if (segment === '..') return null; | |||
| segments.push(segment); | |||
| } | |||
| if (!segments.length) return null; | |||
| return '/' + segments.join('/'); | |||
| } | |||
| /* | |||
| * 在独立 worker 中打开一层压缩包并流式吐出条目; | |||
| * 返回其中根级嵌套压缩包(下一层包装)的原始字节。 | |||
| */ | |||
| function extractLayer(file, onEntry) { | |||
| return new Promise((resolve, reject) => { | |||
| const worker = new Worker(WORKER_URL); | |||
| const nested = []; | |||
| let settled = false; | |||
| const finish = (error) => { | |||
| if (settled) return; | |||
| settled = true; | |||
| worker.terminate(); | |||
| if (error) reject(error); | |||
| else resolve(nested); | |||
| }; | |||
| worker.addEventListener('error', (event) => { | |||
| finish(new Error(event.message || 'libarchive worker failed')); | |||
| }); | |||
| worker.addEventListener('message', ({ data }) => { | |||
| if (data.type === 'ERROR') { | |||
| finish(new Error(data.error && data.error.message ? data.error.message : 'libarchive failed')); | |||
| return; | |||
| } | |||
| if (data.type === 'READY') { | |||
| worker.postMessage({ type: 'OPEN', file }); | |||
| return; | |||
| } | |||
| if (data.type === 'OPENED') { | |||
| worker.postMessage({ type: 'EXTRACT_FILES' }); | |||
| return; | |||
| } | |||
| if (data.type === 'END') { | |||
| finish(null); | |||
| return; | |||
| } | |||
| if (data.type !== 'ENTRY') return; | |||
| const entry = data.entry; | |||
| if (entry.type !== 'FILE' || !entry.fileData) return; | |||
| // 根级压缩包是下一层包装,不是站点内容 | |||
| if (entry.path.replace(/\\/g, '/') === 'MayxBlog.7z') { | |||
| nested.push(entry.fileData); | |||
| return; | |||
| } | |||
| onEntry(entry); | |||
| }); | |||
| }); | |||
| } | |||
| /* 几十 MB 的缓存是浏览器常见的回收对象,而重新解包代价高,尽量申请持久存储。 */ | |||
| async function requestPersistence() { | |||
| if (!navigator.storage || !navigator.storage.persist) return false; | |||
| try { | |||
| if (await navigator.storage.persisted()) return true; | |||
| return await navigator.storage.persist(); | |||
| } catch { | |||
| return false; | |||
| } | |||
| } | |||
| /* 读取就绪标记(= 离线模式状态);损坏或缺清单的标记视为不存在。 */ | |||
| export async function readStatus() { | |||
| const cache = await caches.open(CACHE_NAME); | |||
| const hit = await cache.match(READY_KEY); | |||
| if (!hit) return null; | |||
| const meta = await hit.json().catch(() => null); | |||
| if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null; | |||
| return meta; | |||
| } | |||
| /* | |||
| * 线上压缩包的大小即版本号。HEAD 不被 SW 拦截,拿到的一定是线上真实值; | |||
| * 网络失败返回 null,调用方选择信任现有缓存而不是误伤。 | |||
| */ | |||
| async function payloadSize() { | |||
| try { | |||
| const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' }); | |||
| if (!response.ok) return null; | |||
| const size = Number(response.headers.get('content-length')); | |||
| return Number.isFinite(size) && size > 0 ? size : null; | |||
| } catch { | |||
| return null; | |||
| } | |||
| } | |||
| /* | |||
| * 控制页打开时调用:未启用 → {ready:false};已启用且版本一致 → {ready:true, meta}; | |||
| * 版本不一致 → 清空缓存并返回 {ready:false, stale:true}(已回退在线模式)。 | |||
| */ | |||
| export async function ensureFresh() { | |||
| const cache = await caches.open(CACHE_NAME); | |||
| const size = await payloadSize(); | |||
| const meta = await readStatus(); | |||
| if (!meta) return { ready: false, size, stale: false }; | |||
| if (size && meta.payloadSize !== size) { | |||
| await caches.delete(CACHE_NAME); | |||
| return { ready: false, size, stale: true }; | |||
| } | |||
| return { ready: true, size, stale: false, meta }; | |||
| } | |||
| /* | |||
| * 启用离线模式:注册 SW → 清空旧缓存(同时清掉上次中断的残留,保证随后 | |||
| * 的压缩包下载不会被旧缓存喂旧的 quine 副本)→ 下载并逐层解包 → 写标记。 | |||
| */ | |||
| export async function activateOffline({ onProgress } = {}) { | |||
| if (!('serviceWorker' in navigator)) { | |||
| throw new Error('当前浏览器不支持 Service Worker,无法启用离线模式。'); | |||
| } | |||
| await navigator.serviceWorker.register('/sw.js', { scope: '/' }); | |||
| await navigator.serviceWorker.ready; | |||
| await caches.delete(CACHE_NAME); | |||
| const persisted = await requestPersistence(); | |||
| // 写操作串行链,保证按到达顺序落盘且不产生无界并发 | |||
| const cache = await caches.open(CACHE_NAME); | |||
| let writes = Promise.resolve(); | |||
| const keys = new Set(); | |||
| let files = 0; | |||
| let bytes = 0; | |||
| let skipped = 0; | |||
| const response = await fetch(ARCHIVE_URL, { cache: 'no-store' }); | |||
| if (!response.ok) throw new Error(`无法下载站点压缩包:HTTP ${response.status}`); | |||
| const outer = await response.arrayBuffer(); | |||
| const downloadedBytes = outer.byteLength; | |||
| let layer = [outer]; | |||
| while (layer.length) { | |||
| const bytesThisLayer = layer.shift(); | |||
| const file = new File([bytesThisLayer], 'MayxBlog.7z'); | |||
| layer = await extractLayer(file, (entry) => { | |||
| const key = sitePathFor(entry.path); | |||
| if (!key) { | |||
| skipped += 1; | |||
| return; | |||
| } | |||
| const body = entry.fileData; | |||
| files += 1; | |||
| bytes += body.byteLength; | |||
| writes = writes.then(async () => { | |||
| await cache.put(key, new Response(body, { | |||
| status: 200, | |||
| headers: { 'Content-Type': mimeFor(key) }, | |||
| })); | |||
| keys.add(key); | |||
| }); | |||
| if (onProgress) onProgress({ done: false, files, bytes, skipped }); | |||
| }); | |||
| } | |||
| await writes; | |||
| const meta = { | |||
| files, | |||
| bytes, | |||
| skipped, | |||
| persisted, | |||
| payloadSize: downloadedBytes, | |||
| failed: files - keys.size, | |||
| keys: [...keys], | |||
| unpackedAt: new Date().toISOString(), | |||
| }; | |||
| await cache.put( | |||
| READY_KEY, | |||
| new Response(JSON.stringify(meta), { | |||
| headers: { 'Content-Type': 'application/json; charset=utf-8' }, | |||
| }) | |||
| ); | |||
| if (onProgress) onProgress({ done: true, cached: false, ...meta }); | |||
| return meta; | |||
| } | |||
| /* 切回在线模式:删掉缓存(含标记),SW 随即恢复纯转发。返回之前是否处于离线模式。 */ | |||
| export async function deactivateOffline() { | |||
| const had = await readStatus(); | |||
| await caches.delete(CACHE_NAME); | |||
| return Boolean(had); | |||
| } | |||
| @ -0,0 +1,37 @@ | |||
| --- | |||
| layout: default | |||
| title: 离线模式 | |||
| --- | |||
| # 离线模式 | |||
| 本页用于管理本博客的离线阅读功能: | |||
| - **启用离线模式**:浏览器会下载整站压缩包(`MayxBlog.7z`)并解包到本地缓存,之后即使没有网络也能正常浏览博客; | |||
| - **自动回退**:当站点压缩包更新(博客重新部署)时,离线缓存会自动失效并回退到在线模式,下次重新启用即可获取最新内容; | |||
| - **手动切换**:离线模式下可以随时在本页切回在线模式,切回后页面将重新通过网络加载。 | |||
| <table> | |||
| <tbody> | |||
| <tr><th>当前状态</th><td id="offline-state">检测中…</td></tr> | |||
| <tr><th>缓存版本</th><td id="offline-version">-</td></tr> | |||
| <tr><th>文件统计</th><td id="offline-files">-</td></tr> | |||
| <tr><th>解包时间</th><td id="offline-time">-</td></tr> | |||
| <tr><th>持久存储</th><td id="offline-persist">-</td></tr> | |||
| </tbody> | |||
| </table> | |||
| <p id="offline-progress-box" hidden="hidden"> | |||
| <progress id="offline-progress" max="100" value="0"></progress> | |||
| <small id="offline-progress-text">准备中…</small> | |||
| </p> | |||
| <p><strong id="offline-message"></strong></p> | |||
| <p> | |||
| <button type="button" id="offline-enable">启用离线模式</button> | |||
| <button type="button" id="offline-disable">切换回在线模式</button> | |||
| </p> | |||
| <script src="/assets/js/offline-page.js"></script> | |||
| @ -0,0 +1,140 @@ | |||
| /* | |||
| * Mayx的博客 · 离线模式 Service Worker | |||
| * | |||
| * 两种模式(由 Cache Storage 中是否存在就绪标记决定): | |||
| * - 在线模式(默认,无标记):所有请求原样转发网络,SW 形同虚设。 | |||
| * - 离线模式(/offline.html 启用后写入标记):同源 GET 请求缓存优先, | |||
| * 缓存未命中且清单里没有的路径才回源网络,实现断网阅读。 | |||
| * | |||
| * 版本控制:压缩包 /MayxBlog.7z 的 content-length 即版本号(每次重新部署 | |||
| * 都会变化)。SW 激活时、以及导航请求按 RECHECK_INTERVAL 节流复核; | |||
| * 一旦发现线上压缩包与已缓存版本不一致,立即清空缓存 → 自动回退在线模式。 | |||
| * 网络不可用时复核失败,保留缓存(离线阅读本来就是离线模式的目的)。 | |||
| */ | |||
| const CACHE_NAME = 'mayxblog-offline-v1'; | |||
| const READY_KEY = '/__offline_manifest__'; | |||
| const ARCHIVE_URL = '/MayxBlog.7z'; | |||
| const RECHECK_INTERVAL = 5 * 60 * 1000; | |||
| let lastRecheck = 0; | |||
| self.addEventListener('install', () => { | |||
| self.skipWaiting(); | |||
| }); | |||
| self.addEventListener('activate', (event) => { | |||
| event.waitUntil( | |||
| caches.keys() | |||
| .then((keys) => | |||
| Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) | |||
| ) | |||
| .then(verifyPayloadVersion) | |||
| .then(() => self.clients.claim()) | |||
| ); | |||
| }); | |||
| /* | |||
| * HEAD 请求不被下面的 fetch 处理器拦截,这里读到的一定是线上真实压缩包, | |||
| * 而不是缓存里的 quine 副本。网络失败返回 null,调用方选择信任现有缓存。 | |||
| */ | |||
| async function payloadSize() { | |||
| try { | |||
| const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' }); | |||
| if (!response.ok) return null; | |||
| const size = Number(response.headers.get('content-length')); | |||
| return Number.isFinite(size) && size > 0 ? size : null; | |||
| } catch { | |||
| return null; | |||
| } | |||
| } | |||
| /* | |||
| * 就绪标记同时是清单:记录了解包写入的所有路径。没有它就无法区分 | |||
| * “站点本来就没有这个页面”和“缓存条目被浏览器回收了”。 | |||
| */ | |||
| async function readManifest(cache) { | |||
| const hit = await cache.match(READY_KEY); | |||
| if (!hit) return null; | |||
| const meta = await hit.json().catch(() => null); | |||
| if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null; | |||
| return meta; | |||
| } | |||
| /* 压缩包更新 → 清空缓存(即回退在线模式);返回是否发生了失效。 */ | |||
| async function verifyPayloadVersion() { | |||
| const cache = await caches.open(CACHE_NAME); | |||
| const meta = await readManifest(cache); | |||
| if (!meta) return false; | |||
| const size = await payloadSize(); | |||
| if (size && meta.payloadSize !== size) { | |||
| await caches.delete(CACHE_NAME); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /* 服务器对目录索引的处理与 Jekyll 产物一致,这里补齐候选路径。 */ | |||
| function candidatesFor(pathname) { | |||
| const keys = [pathname]; | |||
| if (pathname.endsWith('/')) { | |||
| keys.push(pathname + 'index.html'); | |||
| } else { | |||
| keys.push(pathname + '/index.html'); | |||
| keys.push(pathname + '.html'); | |||
| } | |||
| return keys; | |||
| } | |||
| self.addEventListener('fetch', (event) => { | |||
| const request = event.request; | |||
| if (request.method !== 'GET') return; // HEAD 等直通网络(版本探测依赖这一点) | |||
| const url = new URL(request.url); | |||
| if (url.origin !== self.location.origin) return; // 跨域请求不干预 | |||
| // 显式 no-store 的请求(重新下载压缩包等)绕过缓存,避免吃到旧的 quine 副本 | |||
| if (request.cache === 'no-store') return; | |||
| event.respondWith(respond(request, url, event)); | |||
| }); | |||
| async function respond(request, url, event) { | |||
| const cache = await caches.open(CACHE_NAME); | |||
| // 导航请求顺带做节流版本复核;失效发生在后台,本次响应不受影响, | |||
| // 下一次请求自然落入在线模式。 | |||
| if (request.mode === 'navigate' && Date.now() - lastRecheck > RECHECK_INTERVAL) { | |||
| lastRecheck = Date.now(); | |||
| event.waitUntil(verifyPayloadVersion()); | |||
| } | |||
| const candidates = candidatesFor(url.pathname); | |||
| for (const key of candidates) { | |||
| const hit = await cache.match(key); | |||
| if (hit) return hit; | |||
| } | |||
| const meta = await readManifest(cache); | |||
| if (!meta) return fetch(request); // 在线模式:纯转发 | |||
| // 清单承诺过这个路径但缓存里没有 → 条目被浏览器回收,缓存已不可信, | |||
| // 整体作废并回退在线模式。 | |||
| if (candidates.some((key) => meta.keys.includes(key))) { | |||
| await caches.delete(CACHE_NAME); | |||
| return fetch(request); | |||
| } | |||
| // 清单之外的路径回源网络;断网时的导航请求用站点 404 页兜底。 | |||
| try { | |||
| return await fetch(request); | |||
| } catch (error) { | |||
| if (request.mode === 'navigate') { | |||
| const notFound = await cache.match('/404.html'); | |||
| if (notFound) { | |||
| return new Response(notFound.body, { status: 404, headers: notFound.headers }); | |||
| } | |||
| } | |||
| throw error; | |||
| } | |||
| } | |||
| @ -0,0 +1,21 @@ | |||
| MIT License | |||
| Copyright (c) 2018 ნიკა | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in all | |||
| copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| SOFTWARE. | |||