diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 40f2b06..0000000 --- a/.editorconfig +++ /dev/null @@ -1,56 +0,0 @@ -# http://editorconfig.org -# Properties: https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties - -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 4 -insert_final_newline = true -trim_trailing_whitespace = true -end_of_line = lf - -# Docstrings and comments use max_line_length = 79 -[*.py] -max_line_length = 119 -indent_size = 4 - -# Use 2 spaces for the HTML files -[*.html] -indent_size = 2 - -[*.js] -indent_size = 4 -quote_type = single -curly_bracket_next_line = false -spaces_around_operators = true -spaces_around_brackets = outside -indent_brace_style = K&R - - -# The JSON files contain newlines inconsistently -[*.json] -indent_style = space -indent_size = 2 -insert_final_newline = ignore - -# Minified JavaScript files shouldn't be changed -[**.min.js] -indent_style = ignore -insert_final_newline = ignore - -# Makefiles always use tabs for indentation -[Makefile] -indent_style = tab -indent_size = 8 - -# Batch files use tabs for indentation -[*.bat] -indent_style = tab - -[**.txt] -max_line_length = 79 - -[*.md] -trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..06bfac8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI +on: + push: + branches: [master, main] + pull_request: + workflow_dispatch: +jobs: + unit: + name: Unit tests (node ${{ matrix.node }}) + runs-on: ubuntu-latest + strategy: + matrix: + node: [22, 24] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: make check + - run: make test + e2e: + name: Browser e2e (Chrome for Testing) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - name: Install Chrome for Testing into the holodeck + id: chrome + run: | + mkdir -p "$HOME/.holodeck/browsers" + out=$(npx --yes @puppeteer/browsers install chrome@stable --path "$HOME/.holodeck/browsers") + echo "$out" + echo "path=${out#* }" >> "$GITHUB_OUTPUT" + - name: Run e2e + env: + BROWSER: ${{ steps.chrome.outputs.path }} + run: make e2e + package: + name: Package zip + runs-on: ubuntu-latest + needs: [unit, e2e] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: make package + - uses: actions/upload-artifact@v4 + with: + name: extension-zip + path: "*.zip" + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 1a07445..dbfbac7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store -dist/ +*.zip +node_modules/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0a8a426 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,23 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "tabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "rangeStart": 0, + //"rangeEnd": Infinity, + //"parser": null, + //"filepath": null, + "requirePragma": false, + "insertPragma": false, + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "vueIndentScriptAndStyle": false, + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..94e3b0f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# Marginalia (Page Notes): notes for AI agents and contributors + +Built from the Hacktoolkit Chrome extension skeleton. Vanilla JavaScript, Manifest V3, no build step, no runtime dependencies, no server. Keep it that way. + +## Rules of the road + +- **All operations run in the service worker.** The popup, side panel and options page send `chrome.runtime.sendMessage` and render results. Chrome closes the popup when a window is created or focused. +- **The popup and the side panel are one view.** `src/panel.html` is `popup.html` with `body.panel`; `popup.js` branches on that class. The popup is bound to the tab it opened on; the panel follows its own window's active tab (it sends `windowId`). Captures need the activeTab grant from an icon click, so the panel shows a hint on uncaptured pages instead of trying. +- **The display name lives in `manifest.json` only.** `name` is "Marginalia (Page Notes)" and `short_name` is "Marginalia"; code reads them at runtime, the short name for compact spots (pins, widget bar, badge tooltip) and the full name for titles. Internal identifiers (storage keys, the `app: marginalia` frontmatter token, the IndexedDB name `page-notes`, the `pn:` message prefix, the share envelope `-----BEGIN MARGINALIA-----`) are fixed strings and do not change with the name. +- **One note per canonical page.** Identity comes from `identify()` in `src/lib/notes.js`, which uses the URL rules in `src/lib/url.js`. Never key anything on a raw URL. +- **Pure logic is tested.** `notes.js`, `profile.js`, `url.js` and `zip.js` have no Chrome APIs and have `node --test` suites. Markdown round-trip (`toMarkdown` / `fromMarkdown`) must stay lossless for entries, tags, reminder and key. +- **Notes live in `chrome.storage.local`**, one item per note plus `notes:index`. Sync storage is capped at 100 KB total; only settings go there. +- **Every capture is a user action.** Captures use the `activeTab` grant from the user's click (saving an entry, opening the popup). No background content scripts, no passive collection, nothing transmitted. This is a product rule, not just a store rule. +- **Captures are history.** `addSnapshot` in `notes.js` appends to `note.snapshots` (capped, first kept, identical text collapses). Site parsers in `src/lib/profile.js` run on captures by rule name via `extractorFor`; add a parser by adding a fixture in `test/fixtures/` and tests first. Parsers must be text-based and tolerant; never DOM selectors. +- **Insights are questions, not verdicts.** Findings carry `level: 'ask' | 'info'` and neutral wording ("ask about transition dates"). Never add a finding that infers intent or a protected characteristic. Promotions at one employer group together; gaps and short stints are `info`. +- **Stickies anchor through `src/lib/anchor.js`** (pure; css rooted at a stable id, text quote with context, structural path; verified by similarity; orphan rather than guess). The in-page layer in `src/content/` runs in a shadow root, is injected with `activeTab` on the user's click, and talks to the service worker only through messages. Never add a content script that runs on every page by default; per-site always-show is an explicit opt-in through optional host permissions. +- **No live LinkedIn in tests.** Parsers are developed against text fixtures only; e2e supplies captures through `args.snapshot`. +- **`src/lib/url.js` is a copy** of the Organize Tabs module; see hacktoolkit/organize-tabs-chrome-extension#12 for the plan to share it. Keep changes in sync until then. +- Code style per `.prettierrc`: 4 spaces, single quotes, semicolons. + +## Testing + +```sh +make test # unit +make e2e # headless browser, every feature +make dev # the holodeck +``` + +Browser testing never touches a real profile. Automated and interactive runs use the **holodeck**, a throwaway sandbox at `~/.holodeck/` (override with `HOLODECK=`): `make dev` opens a persistent profile at `~/.holodeck//` named "🧪 holodeck"; `make e2e` creates a fresh profile under `~/.holodeck/tmp/` and removes it. Agents may launch, inspect and destroy anything under `~/.holodeck/` without asking. Google Chrome's branded build ignores `--load-extension`; the scripts default to Brave and CI uses Chrome for Testing. + +When adding a message type to `background.js`, add a `check(...)` in `scripts/e2e.mjs`. + +## Releasing + +Bump `version` in `manifest.json` and `package.json`, `make package`, upload the zip to the Chrome Web Store dashboard. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/FILES b/FILES index c85fefd..23038a7 100644 --- a/FILES +++ b/FILES @@ -1,6 +1,5 @@ -LICENSE -Makefile +LICENSE.md README.md img manifest.json -dist +src diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..da1c18b --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,147 @@ +# Handoff: Marginalia (Page Notes), repo notes-chrome-extension + +Written 2026-10-15 for the next agent or person picking this up. Read `AGENTS.md` first (rules of the road, testing, the holodeck); this file is the state of play and the open questions. + +## Where things stand + +- **Branch:** `feature/v1-rewrite`. At the original handoff, 19 commits ahead of `master`, all pushed, with **PR #22** open: https://github.com/hacktoolkit/notes-chrome-extension/pull/22. The continuation below adds local, uncommitted changes. +- **CI:** green at the original handoff (unit tests on Node 22 and 24, headless e2e on Chrome for Testing, packaged zip artifact). Not rerun remotely for the local continuation. +- **Tests:** 57 unit tests (`make test`), 94 e2e checks (`make e2e`), both green locally after the continuations. Syntax checks (`make check`, now including content scripts) also pass. +- **Nothing is published.** `master` is still the 2021 Manifest V2 "URL Notes" app, which no longer loads. The store listing kit (`STORE_LISTING.md`, `promo/store/`) is ready but unused. +- The owner (Jonathan) has loaded the branch unpacked in Brave and used it. Feedback so far has all been folded in. + +## Continuation: popup and side-panel reliability + +Code review found that automatic panel refreshes used the same capture-enabled request as a popup opening, and pending drafts relied on a tab's current URL even after navigation. Fixed in `src/popup.js`: + +- Only the initial popup lookup requests a capture. Panel navigation and storage refreshes explicitly pass `capture: false`; the toolbar-click service-worker handler still captures for the panel. +- Drafts carry the displayed page's URL and title, which the worker resolves through the existing canonical identity rules. Saves wait for any in-flight draft and drain newer text before changing pages. +- A failed save leaves the text dirty so blur can retry it. Actions that depend on saving stop on a failed save. +- Refresh responses are versioned so a late result cannot replace a newer page. Typing during a lookup is preserved. Panel events are scoped to its window and current note. +- Eleven additional browser checks cover passive captures, save failure and retry, navigation with a held save response, and out-of-order page lookups. Browser tests ran with `HOLODECK=/private/tmp/page-notes-holodeck` and blocked external DNS, using only the existing text fixtures. + +This continuation did not publish or update the remote PR. + +## Continuation: explorer, stickies and saved-page cues (2026-10-16) + +Implemented the owner's screenshot feedback, also local and uncommitted: + +- `src/lib/explorer.js` and `src/explorer.css` provide a shared Sites → Pages → Notes browser. All notes uses columns at wider sizes; the sidebar uses collapsible site/page groups and an inline preview of dated entries, stickies and captures. Search includes sticky text and quoted anchors. Selection does not open tabs; Open page and Full details are explicit actions sent to the worker via `openNote`. +- The sidebar has a back arrow beside the title to browse saved pages, and a return-to-current-page control. Opening the panel on a browser/extension page starts in the library. Full details now includes stickies too. +- Warm paper colors, folded-corner pins and a persistent on-page control distinguish stickies. `stickyVisibility` stores a global local preference and broadcasts show/hide changes to existing layers; no new host permissions or passive injection. Picking or jumping to a sticky reveals the layer again. Hiding flushes an open sticky's pending edit. The sidebar empty-sticky hint now spans the full row. +- Red toolbar badges count entries plus stickies, with a descriptive tooltip. Capture-only pages keep a neutral dot. The popup/sidebar shows a saved-note banner with a brief arrival highlight that respects reduced motion. +- A one-time service-worker startup migration cleans unread counters from existing LinkedIn note titles and index rows. It preserves canonical identity, timestamps, entries and original capture titles. Tests cover the transformation and storage migration. +- Validation: 57 unit tests, 94 e2e checks, syntax checks and whitespace checks passed. Light/dark and narrow/wide previews were inspected. To regenerate them: `HOLODECK=/private/tmp/page-notes-holodeck E2E_SCREENSHOTS=/private/tmp/page-notes-preview make e2e`. + +Reload the unpacked extension and refresh existing page tabs to pick up the new in-page layer. Naming, release, drag/selection anchoring and shared review remain open. + +## Continuation: hierarchical back, sidebar close, launch modes (2026-10-16) + +Owner feedback from use: the sidebar back arrow looped, and the docked sidebar could not be dismissed. Also local and uncommitted: + +- **Back is a hierarchy, never a toggle.** `src/lib/explorer.js` tracks a level (0 sites, 1 pages of a site, 2 a page) exposed through `root.dataset.level`, with `up()`, `focus({ host, id })`, `depth()` and an `onNavigate` callback. The narrow layout shows one level at a time; the wide one keeps three columns. The trail is a breadcrumb of buttons. In the sidebar the header arrow lands on the current site's page list, the library arrow climbs one level and hides at the top, and returning to the page is a separate footer action (`#return-to-page`). "All notes" in the footer goes to the root. +- **Sidebar close.** `#close-view` in the panel header and library header calls `window.close()`, which closes the side panel. In sidebar mode the worker now clears the action popup (`chrome.action.setPopup({ popup: '' })`) so the icon toggles the panel; a popup would otherwise take the click. Esc does not close the panel (a stray Esc while writing must not), only the popup and the widget. +- **Launch modes.** `settings.launch` is `'popup' | 'sidebar' | 'floating'` (`LAUNCH_MODES` in `settings.js`); `openInSidePanel: true` migrates to `'sidebar'` in `mergeSettings`. `applyLaunchMode()` in the worker sets the popup and the side panel behaviour; `chrome.action.onClicked` captures the page and, in floating mode, injects the layer and sends `pn:widget`. On browser or extension pages in floating mode it opens the side panel instead. Options has a three-way card selector. +- **Floating widget** in `src/content/layer.js`: a draggable `.fab` (icon, sticky count) and a draggable `.widget` window holding an iframe of `src/panel.html?embed=1`, both in the shadow root, positions remembered through the worker message `widgetLayout` (`widget:layout` in storage.local). The page cannot see into the frame. Clicking the icon again toggles the widget; Esc on the page or in the frame closes it; the frame asks the layer to close with `postMessage({ type: 'pn:widget-close' })`, accepted only from the frame's window and the extension origin. `src/panel.html` and `img/icon32.png` are web-accessible for this. +- **Embed mode in `popup.js`:** `EMBED` from the query string, body gets `embed`; bound to `chrome.tabs.getCurrent()`, follows in-page navigations of that tab, never captures (the worker did on the click), and `dismiss()` replaces `window.close()`. +- **Owner reported floating mode not working** after the above. The in-page mechanism was verified in a real headless Brave (a patched copy of the extension with a host permission on the fixture standing in for the activeTab grant; see the approach in the scratch script described here: inject, send `pn:widget`, probe the shadow root, connect to the frame target): the button appears, the frame loads the embedded panel bound to the tab, a second click closes it, and a `frame-src 'none'` CSP does not block the frame. Two upstream causes were fixed: `annotate.js` reused a layer orphaned by an extension reload (its `chrome.runtime.id` is gone, so the worker could never reach it); it now asks `alive()` and reboots, and `boot()` removes stale hosts. And the side panel fallback for non-web pages lost the click gesture behind `await loadSettings()`; the mode is now cached in the worker (`launchMode`, warmed at every worker start) and `chrome.sidePanel.open` is called synchronously. A failed launch marks the tab's badge with `!` and explains in the icon tooltip. If it still fails for the owner, ask which page and whether the badge showed `!`. +- **Pages listed by site.** Across all sites the page list has a heading per site, sorted by host; the column label and the sidebar library header name the level (All notes with counts, the site, or the page and its site). + +## Continuation: linked pages (2026-10-16) + +Owner's ask: link any page to any other across sites (GitHub profile to LinkedIn profile), with a type-search-filter widget over the pages with notes. Local and uncommitted: + +- **Model** (`notes.js`): `note.links` is a list of the other pages' canonical keys, symmetric, written on both notes by `linkNotes` / `unlinkNotes` (pure, tested). Keys resolve to ids with `noteId(key)`, so the frontmatter line `links: [github:user:jontsai]` round-trips and survives import; `mergeNotes` unions links; `indexEntry` carries a `links` count. `store.js` keeps a note that only holds a link (a note is "empty" only without entries, reminder, captures, stickies and links). +- **Worker**: `link` (`{ id | url | tabId, otherId }`, creating the note for a never-noted page), `unlink`, `links` (resolved index rows, a deleted other side comes back as `missing` with its key), `current` returns `linked`, `deleteNote` removes the page from the pages it was linked to. Each has e2e checks. +- **Picker** (`src/lib/picker.js`): `createPagePicker({ exclude, onPick, onClose })`, a combobox over the index: loads once, filters on every typed word across title, site, URL and the latest note (`matchRows`, tested), arrows/Enter/Esc, mouse. Esc stops propagation so it does not close the popup or widget. Used in the popup/sidebar "Linked pages" section (`#add-link`), the explorer preview ("+ Link a page") and nowhere else yet; it is the widget to reuse for any choose-a-page need. +- **UI**: "Linked pages" card in popup, sidebar and widget; click a linked page to show it in the library (sidebar) or open its details (popup), ↗ opens the page, ✕ unlinks. The explorer preview lists links and navigates to them (`explorer.show(id)`); the details page lists them read-only. Obsidian-style `[[wikilinks]]` in the Markdown body would be a natural follow-up; keys in frontmatter were chosen to keep the round-trip lossless. + +## Continuation: two-way vault and shareable stickies (2026-10-16) + +Owner's asks: use the extension in several browser profiles with notes synced through the iCloud Drive vault folder (each profile on its own schedule, files readable by any browser; SQLite was floated for speed), and send stickies to someone as a flat file with compression and encryption, no server. + +- **Format decision:** Markdown files stay the storage format. SQLite in iCloud Drive with two writers is a corruption hazard and needs a WASM dependency; speed comes from a metadata-only scan instead. `src/lib/vault.js` (pure, tested): `syncFolder(folder, store, state)` lists `{ name, mtime, size }`, reads a file only when mtime or size changed, compares by FNV hash before parsing (cloud drives re-stamp mtimes), merges with `mergeNotes`, and writes a file only when it differs from the note's serialization, so profiles converge without ping-pong. Local changes made while the vault was unreachable are caught by comparing the recorded hash with the note's current serialization. Deletions propagate both ways through the per-profile state (`vault:state`, `{ files: { name: { id, mtime, size, hash } } }`). A note's file follows its title (old name removed). Capture-only notes get no file. `memoryFolder` is the test and e2e seam (`syncVault` accepts `args.folder` and returns the folder and state); `handleFolder` wraps a FileSystemDirectoryHandle. +- **Worker:** `persist` writes through the adapter and records the file; `deleteNote` removes the file; `syncVault` is serialised and runs from a per-profile alarm (`vault:sync`, every 5 minutes), at startup, on "Sync now", after re-allowing access, and at most once a minute when the popup or panel looks at a page. `vault:status` holds the last result and report; `current` returns it so the popup can show the once-per-session "Allow" banner (`#vault-hint`, calls `requestPermission` from the page, then `syncVault`). Permission cannot be requested from the worker. +- **Sharing:** `src/lib/share.js` (pure, tested in node 22+): `sharePayload` (stickies, entries optional), `encodeShare`/`decodeShare` (JSON → deflate-raw via CompressionStream → optional PBKDF2-SHA256 (250k) + AES-256-GCM with random salt and iv → base64 at 76 columns, wrapped in BEGIN/END lines, header `pagenotes/1 deflate aes-gcm|none`), `inspectShare`, `applyShare` (stickies by id, entries by minute+text, idempotent). Worker: `shareExport { id, passphrase, includeEntries }`, `shareImport { text, passphrase, url? }` resolving the page by the recipient's rules and creating the note if needed. UI: "Share…" and "Import…" in the stickies card (popup, sidebar, widget), copy or download `.pagenotes.txt`; Settings imports a file. Anchors travel with the stickies; on a page that differs they show as orphans. +- **Event-driven sync (owner's follow-up).** `FileSystemObserver` is available in this Brave (Chromium 155) in extension pages but not in service workers; verified in the holodeck. So: `observeHandle(handle, onChanges)` and `changeCollector` in `vault.js` (pure, tested), the side panel / widget page (`popup.js`) and Settings (`options.js`) watch the vault folder while open and ask the worker for a scoped sync (`syncVault { only: [names] }`), which `syncFolder` honours by `stat`-ing just those files and handling reported deletions, skipping the write-out of unrelated notes. The worker has the same watch code (`watchVault`, re-armed on every wake) which is a no-op until a browser exposes the API to workers. The 5-minute alarm and the once-a-minute look-at-a-page sync remain the safety net. e2e drives the real observer over the origin-private file system to pin the record shape. +- Tests: 74 unit, 167 e2e checks (launch modes and the popup/behaviour pair, legacy migration, `widgetLayout`, the embedded page's Esc, the layer's button and window including drag by synthetic pointer events, and the sidebar's hierarchical navigation and close control). + +## What it is + +A Manifest V3 extension, vanilla JavaScript, no build step, no dependencies, no server. One note per page, with page identity from URL rules so a note on a PR shows on its Files tab. Features, all shipped on the branch: + +- Editor that is today's entry, autosaving; earlier entries edit in place. +- Captures: on every popup open, the page's title, metadata and text are stored locally as history; changed-since-last-visit diff. +- Profile insights: a text-based LinkedIn parser and consistency analysis (overlapping roles, two current roles, backwards dates, short stints, gaps, truncation, stated years, edits between visits), a one-line card grouped by employer. +- Stickies: comments anchored to page elements (css path rooted at a stable id, text quote with context, structural path), an in-page layer in a shadow root, pick mode, feedback export as a numbered list with quotes. +- Reminders and snoozed tabs as one-click chips; selection capture from the context menu. +- Side panel: the same view beside the page, following the active tab. +- Markdown with YAML frontmatter: zip export with captures, import with merge, Obsidian vault folder connection. +- Options page: all notes with search, reminders, backup and vault, settings with a rules tester. + +## Decisions already made (do not relitigate without the owner) + +- **Name: decided 2026-10-17.** `name` is "Marginalia (Page Notes)" and `short_name` is "Marginalia" in `manifest.json`; code reads them at runtime, the short name for compact spots (on-page pins, widget bar, badge tooltip, empty header) and the full name for titles and the store. Both plain names are taken in the Chrome Web Store (a "Page Notes" with 10k+ users; a "Marginalia" study-notes extension; Marginalia Search), so the combined name is deliberate: brand Marginalia, discoverable as page notes. User-visible format tokens were renamed before any file existed: share envelope `-----BEGIN/END MARGINALIA-----`, header `marginalia/1`, file suffix `.marginalia.txt`, frontmatter `app: marginalia`. Internal keys (IndexedDB `page-notes`, `data-page-notes`, `__pageNotesLayer`, `pn:` messages, storage keys) stay as they are by rule. The repo stays `notes-chrome-extension`; the owner's iCloud Drive vault folder is `htk_marginalia` (renamed from `browser_notes` on 2026-10-17; each profile reconnects after a rename, since Chromium keeps directory handles by path) and nothing in code depends on it. +- **Site names stay in titles.** Only leading unread counters like "(3)" are stripped. The owner asked for this explicitly. +- **Every capture is a user action.** No background content scripts, no passive collection, nothing transmitted. Per-site "always show stickies" is an explicit opt-in through optional host permissions requested from the popup. This is a product rule. +- **Insights are questions, never verdicts.** Findings are `ask` or `info` with neutral wording. Never infer intent or protected characteristics. Never reference the owner's private hiring repository anywhere in this codebase. +- **No live LinkedIn, ever, in tests or development.** Parsers are built against `test/fixtures/*.txt` only. LinkedIn's bot detection is strong and the owner forbade it. +- **Notes in `chrome.storage.local`**, settings in sync (sync is capped at 100 KB total). +- **`src/lib/url.js` is a copy** of the Organize Tabs module. Keep in sync by hand until hacktoolkit/organize-tabs-chrome-extension#12 extracts it. +- **UX bar:** "smart, responsive, minimal clicking, avoid forms, autosave, automagic", polish at the level of Airbnb, Figma, Gusto. The shared token system is `src/ui.css`; refine it, do not replace it. + +## Open questions and likely next asks + +1. The name (above). +2. Merge and release: tag `v1.0.0`, GitHub Release with the CI zip, bump the submodule in hacktoolkit/chrome-extensions, upload to the Chrome Web Store using `STORE_LISTING.md`. Permissions that will draw review questions: `scripting`, `notifications`, `unlimitedStorage`, `sidePanel`, and the optional host permissions; justifications are in the kit. +3. Stickies polish once used for real: drag to reposition, a sticky on a text selection rather than a whole element, export to a shareable page. +4. Options now has a responsive saved-page explorer; the remaining settings sections still use the original card layout. +5. A second site parser (GitHub profile, Crunchbase) would follow the `extractorFor` pattern in `src/lib/profile.js`, fixture first. + +## How to work on it + +```sh +make test # unit, seconds +make e2e # headless browser, every feature, about 3 minutes +make dev # the holodeck: throwaway Brave profile with the extension loaded +make screenshots # README images and 1280x800 store screenshots +make package # zip for the store +``` + +- **The holodeck** (`~/.holodeck/`) is the only place browser tests run. Never test against a real profile. See `AGENTS.md`. +- Google Chrome's branded build ignores `--load-extension`; use Brave or Chrome for Testing (CI installs the latter via `@puppeteer/browsers`). +- The e2e suite blocks DNS (`MAP * ~NOTFOUND, EXCLUDE 127.0.0.1`) and serves a fixture page plus `src/` from a local http server so the in-page layer runs in a real DOM with a stubbed `chrome.*`. Captures in e2e are supplied through `args.snapshot` because `activeTab` needs a real click. +- Pushing to hacktoolkit repos from the owner's machine needs the `jontsai` SSH key explicitly: `GIT_SSH_COMMAND="ssh -i ~/.ssh/jontsai-dataro-id_ed25519 -o IdentitiesOnly=yes" git push ...`. The agent offers another account's key first. +- Chain shell commands carefully: `make e2e 2>&1 | grep ...` exits with grep's status, so `&& git commit` after it commits on failure. Write the log to a file and test for `ALL PASSED`. + +## Layout + +``` +manifest.json MV3, module service worker, popup, side panel, options, commands +src/background.js every operation: messages, badge, reminders, snooze, capture, stickies, layer injection +src/lib/notes.js note model, today's entry, captures, stickies, Markdown round-trip, feedback export (tested) +src/lib/profile.js LinkedIn parser, consistency analysis, edit detection, text diff (tested) +src/lib/anchor.js how a sticky finds its element again (tested on a fake DOM) +src/lib/url.js URL identity rules, copied from Organize Tabs (tested) +src/lib/store.js storage.local notes and index (serialised writes), Obsidian vault handle +src/lib/settings.js settings in sync with local fallback +src/lib/zip.js minimal zip writer (tested) +src/lib/ui.js el/send/fmt shared by the pages +src/content/ annotate.js bootstrap + layer.js (shadow-root pins, popover, pick mode) +src/popup.* the note for the current page; src/panel.html is the same view for the side panel +src/note.* one note in full: insights, changes, entries, captures +src/options.* all notes, reminders, backup and vault, settings, help +scripts/e2e.mjs headless browser suite; scripts/dev.mjs holodeck; scripts/screenshots.mjs +test/ node --test suites and LinkedIn text fixtures +STORE_LISTING.md store copy, permission justifications, privacy answers +``` + +## The wider picture + +This extension is one of the Hacktoolkit Chrome extensions (collection repo: hacktoolkit/chrome-extensions, see its `AGENTS.md`): + +- **organize-tabs-chrome-extension**: v2.1.0 in the store; reference implementation. Open: PR #13 (one-line `ui.css` fix), issue #12 (share the URL rules module). +- **skeleton-chrome-extension**: the template new extensions start from; this extension was built on it. +- **paywall-xray-chrome-extension**: next candidate for modernisation; not publishable to the store. + +Blog posts about the Organize Tabs rebuild are at https://www.jontsai.com/2026/10/10/organize-tabs-2-0 and https://www.jontsai.com/2026/10/11/building-organize-tabs-2-0-with-claude-code and describe the working process the owner likes: review first, constraints from use, real browser tests, an adversarial review before shipping. diff --git a/LICENSE b/LICENSE.md similarity index 96% rename from LICENSE rename to LICENSE.md index efee985..1987bb7 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Hacktoolkit +Copyright (c) 2020 Jonathan Tsai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index dcf763f..1dabbca 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,39 @@ +.PHONY: help test check e2e dev dev-clean screenshots clean package +VERSION := $(shell node -p "require('./manifest.json').version") +NAME := $(shell node -p "require('./manifest.json').name.toLowerCase().replace(/[^a-z0-9]+/g,'_')") + ## help - Display help about make targets for this Makefile help: @cat Makefile | grep '^## ' --color=never | cut -c4- | sed -e "`printf 's/ - /\t- /;'`" | column -s "`printf '\t'`" -t -## install - install dependency packages -install: - cd app && yarn install +## test - run the unit tests +test: + node --test + +## check - syntax-check every script +check: + @for f in src/*.js src/lib/*.js src/content/*.js scripts/*.mjs; do node --check $$f || exit 1; echo "ok $$f"; done + +## e2e - run the browser end-to-end test in a fresh holodeck profile +e2e: + node scripts/e2e.mjs + +## dev - launch the holodeck (throwaway browser profile at ~/.holodeck) with the extension and sample tabs +dev: + node scripts/dev.mjs -## clean - clean previous builds +## dev-clean - end program: delete the holodeck browser profiles +dev-clean: + rm -rf "$${HOLODECK:-$$HOME/.holodeck}"/brave "$${HOLODECK:-$$HOME/.holodeck}"/chromium "$${HOLODECK:-$$HOME/.holodeck}"/tmp + +## clean - remove all build artifacts clean: - rm -rf app/out/* - rm -rf dist - rm -rf *.zip - -## build - build the app for release -build: clean install - cd app && yarn build - mv app/out dist - -## package - creates a new package -package: build - 7z a url_notes.zip `cat FILES` + rm -f *.zip + +## package - build -.zip for the Chrome Web Store +package: clean test + zip -r -X $(NAME)-$(VERSION).zip `cat FILES` -x '*.DS_Store' + +## screenshots - render README images and Chrome Web Store screenshots from the holodeck +screenshots: + node scripts/screenshots.mjs diff --git a/README.md b/README.md index d6c5252..5652dbf 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,107 @@ -# Notes Chrome Extension - -**The purpoose of this application is to allow users to easily take and store notes on URLs directly on the browser.** - -These are the core components of the application. - -1. Basic UI to take notes. -2. UI to see previous notes. -3. Local storage -4. E2E Encryption -5. Cloud Sync - -**Relevant Documentation Links** -[Chrome Developer Docs](https://developer.chrome.com/docs/extensions/mv3/) -[Sample Chrome Extensions](https://github.com/GoogleChrome/chrome-extensions-samples) -[Encryption Framework](https://keys.pub/) - -The core languages being used will be HTML, CSS, and JavaScript. -Eloquent JavaScript - Execellent resource and available online for free. -Let's use Flexbox for styling layouts. - -## Development and Testing - -1. Clone this repository locally. -1. In Google Chrome, go to `Manage Extensions` (). -1. Enable `Developer mode`. -1. Press `Load unpacked` and navigate to the folder with `manifest.json`, and `Select`. -1. Voilà! - -## Building and publishing - -See: https://developer.chrome.com/webstore/publish - - -## Contributing - -Contributions are welcome. Please fork the repository and submit a pull request from your local branch. - -## Support - -Check out other Chrome extensions made by Hacktoolkit! +# Marginalia (Page Notes) + +Notes in the margins of the web. A note on any page follows that page across URL variants, with dated entries, reminders, snoozed tabs, page snapshots, and Markdown export that Obsidian reads natively. + +Vanilla JavaScript, Manifest V3, no build step, no dependencies, no server. Nothing leaves your browser. Works in Chrome, Brave, Edge and other Chromium browsers (Chrome 121 or newer). + +

+ Popup + Reminders and snooze +

+

Note detail with insights, changes and captures

+

Stickies pinned to parts of a pricing page, one open for editing

+ +## What it does + +- **One note per page, many dated entries.** Click the toolbar icon on any page and type. It saves as you type; there is no Save button. Each day you write becomes a dated entry, and earlier entries edit in place with a click. A red toolbar badge counts entries and stickies on the current page; the sidebar highlights saved notes when you return. +- **Browse your notebook.** All notes is a Sites → Pages → Notes explorer, with three columns on a wide screen and one level at a time in the sidebar. Across all sites, pages are listed by site. Navigation is a hierarchy: the back arrow beside the sidebar's page title goes up to that site's saved pages, the arrow in the library climbs one more level and disappears at the top, and a breadcrumb jumps to any ancestor. "Current page" in the footer returns to the page you are on. Preview dated entries, stickies and captures, then explicitly open a page when you need it. +- **Link pages across sites.** Connect any saved page to any other when it makes sense to you: your GitHub profile to your LinkedIn, a ticket to its pull request. "+ Link" opens a type-to-find picker over your saved pages; linked pages show on both sides with their site, open in a click, and are carried in the Markdown frontmatter by canonical key so export and import keep them. Nothing is linked for you. +- **Open it your way.** In Settings, choose what the toolbar icon does: a popup next to the icon, a docked sidebar that follows the active tab and closes with its ✕ or another click on the icon, or a floating button on the page itself. The floating button and the window it opens can both be dragged anywhere and remember where you left them; the window is the extension's own page in a frame, so the site around it can neither read nor restyle your notes. +- **Stickies on the page.** Pin warm paper-style comments to page elements. The on-page control and popup's Show/Hide all button toggle stickies across open tabs; visibility is remembered after a reload. Adding or jumping to a sticky shows them again. Quotes and comments can be copied as a numbered feedback list. +- **The note follows the page.** A pull request and its Files tab, a Jira issue and its board link, a LinkedIn profile and its Experience page, a Google Doc in edit and view mode: one note. Nineteen built-in identity rules, the same engine as [Organize Tabs](https://github.com/hacktoolkit/organize-tabs-chrome-extension), editable with a live tester. +- **Capture a quote.** Select text, right-click, "Add selection to this page's note". +- **Remind me.** One click under the editor: in an hour, tonight, tomorrow, next week, or pick a time. The page reopens with a notification. +- **Snooze a tab.** One click: close it now, get it back tomorrow morning. Also a keyboard shortcut and a right-click item. +- **Captures.** Every time you open the popup on a page, what the page looked like is kept locally: title, core metadata (canonical URL, site, author, published date, JSON-LD types) and the page text. Identical captures collapse into one. So you have a local history of what you actually saw, and the note still makes sense if the page changes or vanishes. +- **Changed since your last visit.** When a page differs from its previous capture, the popup says so and the note's detail page shows the added and removed lines. +- **Profile insights.** On a LinkedIn profile the capture is parsed into a work history, and the extension points out what a careful reader would ask about: two full-time roles that overlap, two current full-time roles, dates that run backwards, short stints, long gaps. It also summarises years of experience, employers and average tenure, and builds a one-line card grouped by employer with promotions in order, ready to copy. Edits to the history between visits, such as an end date that moved, are called out separately. These are prompts for a conversation, never verdicts. +- **Markdown export.** Every note is a `.md` file with YAML frontmatter. Download them all as a zip, which also carries every capture as plain text under `captures/`, or import notes back. +- **A vault folder shared between browsers.** Connect a folder, in your Obsidian vault or in a synced drive such as iCloud Drive, and your notes live there as Markdown files: every save is written at once, and changes from other profiles are picked up as they land: while the sidebar, the floating widget or Settings is open, the folder is watched and only the changed files are read, with a check every five minutes as the safety net. Connect the same folder from each profile and they share one notebook; deletions follow too. The scan reads only file metadata and parses a file only when its content changed, so it stays quick with hundreds of notes, and there is no database to corrupt: the files are plain Markdown any app can read. The folder connection uses the File System Access API, which Chrome and Edge have on by default; Brave turns it off, and the settings page shows the flag to enable it. The browser asks once per session to re-allow access, and the popup offers the click. +- **Send stickies to someone.** "Share…" on a page turns your stickies, and your notes if you tick the box, into a small text file: JSON, deflate-compressed, base64, wrapped between `-----BEGIN MARGINALIA-----` and `-----END MARGINALIA-----` so it survives email and chat. Add a passphrase and it is encrypted with AES-256-GCM (key from PBKDF2-SHA256); without one it is readable by anyone with the extension. The recipient pastes it into "Import…" or drops the file into Settings, and the stickies land on their note for that page under their own identity rules, merged by id so nothing is overwritten. No server, no account. +- **Search** across titles, URLs, tags, entries, sticky comments and their quoted text, and a type-to-find picker wherever you choose a page. Previously saved LinkedIn titles automatically lose leading unread-message counts; historical captures and note dates are preserved. + +## The note format + +```markdown +--- +title: "Jane Doe | LinkedIn" +url: https://www.linkedin.com/in/janedoe/ +key: linkedin:in/janedoe +rule: "LinkedIn profile" +created: 2026-10-11T03:02:00.000Z +updated: 2026-10-18T22:30:00.000Z +tags: [people, sales] +remind: 2026-11-01T09:00:00.000Z +excerpt: "Jane Doe - Head of Things at Acme" +app: marginalia +--- + +## 2026-10-19 09:30 + +Renamed profile to "Jane D." Still at Acme. + +## 2026-10-11 14:02 + +Likes cheese. Two kids, dog named Fluffy. +``` + +`key` is the page's canonical identity from the rules, which is what makes the note follow the page. Entries are `##` headings with a local timestamp, newest first in the file. Hand-written files with the same shape import fine, and text before the first heading becomes an undated entry. + +## Privacy + +Everything is stored in your browser profile with `chrome.storage.local`. No network requests, no accounts, no analytics. The in-page layer (sticky pins, and the floating button in that launch mode) is injected only when you click the icon or add a sticky on that page; the one exception is a site where you turned on "Always show stickies", which asks the browser for permission on that site alone. A page is captured only when you act on it, saving a note or opening the popup there, using the `activeTab` grant that your click provides, and the capture never leaves the browser. There are no background content scripts and nothing runs on pages you merely visit. Export and the Obsidian vault connection write files only where you choose. + +Profile insights are computed locally from the text you captured, phrased as questions, and never make inferences about protected characteristics. Overlapping dates have ordinary explanations; the point is to know what to ask. + +## Development + +```sh +make test # unit tests: note model, Markdown round-trip, zip writer, URL rules +make e2e # every feature in a real headless browser +make dev # the holodeck: a throwaway profile with the extension loaded +make package # zip for the Chrome Web Store +``` + +Browser testing never touches a real profile; see `AGENTS.md` for the holodeck. Loading unpacked into your real profile: `chrome://extensions`, Developer mode, **Load unpacked**, pick this folder. + +### Layout + +``` +src/background.js service worker: messages, presence badge, reminders, snooze, context menu, export/import +src/lib/notes.js pure note model, captures and Markdown (tested) +src/lib/profile.js pure profile parser, consistency analysis, edit detection, text diff (tested) +src/lib/url.js URL identity rules, copied from Organize Tabs (tested) +src/lib/anchor.js how a sticky finds its element again: css, quote, path (tested) +src/content/ the in-page layer: pins, popover, pick mode, in a shadow root +src/lib/store.js chrome.storage.local notes and index; Obsidian vault handle +src/lib/zip.js minimal zip writer for export (tested) +src/lib/settings.js settings in storage.sync with local fallback +src/popup.* the note for the current page +src/options.* all notes, reminders, backup and vault, settings, help +src/note.* one note in full: insights, changes, entries, captures +src/lib/ui.js shared DOM and messaging helpers for the three pages +src/lib/explorer.js shared saved-page explorer for All notes and the sidebar +src/explorer.css responsive columns and accordion layout +scripts/screenshots.mjs README images and 1280x800 store screenshots (make screenshots) +STORE_LISTING.md Chrome Web Store copy, permission justifications, privacy answers +scripts/e2e.mjs headless browser suite +scripts/dev.mjs holodeck launcher +``` + +## History + +This repository began in 2021 as "URL Notes", a Next.js app in a Manifest V2 shell. Version 1.0 is a rewrite on the Hacktoolkit extension skeleton. ## License -MIT. See LICENSE. +MIT. See LICENSE.md. diff --git a/STORE_LISTING.md b/STORE_LISTING.md new file mode 100644 index 0000000..078a566 --- /dev/null +++ b/STORE_LISTING.md @@ -0,0 +1,42 @@ +# Chrome Web Store listing kit + +Paste-ready copy for the dashboard. Plain-text copies are in `promo/store/`. The display name comes from `manifest.json`: "Marginalia (Page Notes)", short name "Marginalia". Both plain names are taken in the store (a "Page Notes" with 10k+ users, a "Marginalia" study-notes extension), so the combined name is deliberate: the brand is Marginalia, and a search for page notes still finds it. + +## Store listing tab + +**Summary** (from `manifest.json` → `description`, max 132 characters): see the manifest. + +**Category:** Productivity → Workflow & Planning. **Language:** English. + +**Detailed description:** `promo/store/description.txt`. + +**Screenshots:** 1280x800, from `promo/store/` once generated. + +## Privacy tab + +**Single purpose:** + +> Marginalia (Page Notes) lets the user write notes attached to web pages, keeps a local record of what those pages looked like when the user was there, and reminds the user to return to pages at a chosen time. + +**Permission justifications:** + +| Permission | Justification | +| --- | --- | +| `tabs` | Read the current tab's URL and title to attach a note to the page, show a count on the toolbar icon, and reopen a page when a reminder fires or a snoozed tab is due. | +| `activeTab` and `scripting` | When the user opens the popup or adds a sticky on a page, read that page's title, metadata and visible text so the note keeps a local record of what the user saw, and show the in-page layer on that page: sticky pins and comments, and, if the user chose the "on the page" launch mode, a floating button that opens the note in a frame. Runs only on the user's click, on that tab, never in the background. | +| `storage` and `unlimitedStorage` | Notes and page captures are stored locally in the browser profile; captures of long pages can exceed the default quota. Settings sync with the profile. | +| `alarms` | Fire reminders and reopen snoozed tabs at the time the user chose. | +| `notifications` | Show the reminder when it fires. | +| `contextMenus` | "Add selection to this page's note" and "Snooze this tab" in the right-click menu. | +| `sidePanel` | Show the note beside the page in the browser's side panel, following the active tab. | +| Optional host permissions `https://*/*`, `http://*/*` | Only when the user ticks "Always show stickies on this site": requested for that one origin so the sticky pins can appear on every visit there without a click. Never requested otherwise. | + +**Remote code:** No. + +**Data usage:** The extension does not collect or transmit any user data. Notes and page captures stay in the browser's local storage. Export, the vault folder and shared files are written only where the user chooses; a shared file is produced only on the user's explicit action and leaves the browser only by the user's own copy or download. Tick the three certifications. + +**Privacy policy URL:** the README's Privacy section, https://github.com/hacktoolkit/notes-chrome-extension#privacy + +## Distribution tab + +Free, public, all regions. diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 20fccdd..0000000 --- a/app/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local diff --git a/app/README.md b/app/README.md deleted file mode 100644 index 02695bc..0000000 --- a/app/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a starter template for [Learn Next.js](https://nextjs.org/learn). \ No newline at end of file diff --git a/app/components/NoteTableItem.js b/app/components/NoteTableItem.js deleted file mode 100644 index 5ad504e..0000000 --- a/app/components/NoteTableItem.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -const NoteTableItem = ({ noteData, deleteRowFromTable }) => { - return ( -
- - -

- {noteData.title} -

- - - -

- {noteData.url} -

-
- - -

- {noteData.body} -

- - - - - - -
- ); -}; - -export default NoteTableItem; diff --git a/app/components/base_page.js b/app/components/base_page.js deleted file mode 100644 index 4507957..0000000 --- a/app/components/base_page.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import Head from 'next/head'; - -import css from '../styles/common.module.scss'; - -function BasePage(WrappedComponent) { - return class extends React.Component { - constructor(props) { - super(props); - } - - render() { - return ( -
- - Notes Chrome Plugin - - - - - - -
- -
-
- ); - } - }; -} - -export default BasePage; diff --git a/app/components/base_popup.js b/app/components/base_popup.js deleted file mode 100644 index 6635c73..0000000 --- a/app/components/base_popup.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import { Alert, Nav, Navbar } from 'react-bootstrap'; - -import Head from 'next/head'; - -import { canAccessChromeTabs, getCurrentTab } from '../lib/chrome_tabs'; -import { defaultNotesForTab, getNotesForUrl } from '../lib/notes'; -import isDev from '../lib/is_dev'; - -import css from '../styles/common.module.scss'; - -function BasePopup(WrappedComponent) { - return class extends React.Component { - constructor(props) { - super(props); - - this.state = { - alert: null, - currentTabUrl: null, - currentNotes: null, - }; - } - - componentDidMount() { - if (canAccessChromeTabs()) { - this.fetchCurrentTabData(); - } - } - - fetchCurrentTabData() { - const self = this; - - getCurrentTab((currentTab) => { - const url = currentTab.url; - self.setState({ - currentTabUrl: url, - }); - - const notesFallback = defaultNotesForTab(currentTab); - - getNotesForUrl(url, notesFallback, (notes) => { - self.setState({ - currentNotes: notes, - }); - }); - }); - } - - addAlert(heading, message, alertType) { - this.setState({ - alert: { heading, message, alertType }, - }); - } - - showSuccessAlert() { - this.addAlert( - 'Saved Successfully', - 'The notes for this page were saved properly in All Notes.', - 'success' - ); - } - - showErrorAlert() { - this.addAlert( - 'Error Saving', - 'The notes for this page were NOT saved properly.', - 'danger' - ); - } - - render() { - const extension = isDev() ? '' : '.html'; - - const alertJSX = this.state.alert ? ( - this.setState({ alert: null })} - dismissible - > - {this.state.alert.heading} -

{this.state.alert.message}

-
- ) : null; - - return ( -
- - Notes Chrome Plugin - - - - - - - - Note Taker - - -
- -
-
{alertJSX}
-
- ); - } - }; -} - -export default BasePopup; diff --git a/app/lib/chrome_tabs.js b/app/lib/chrome_tabs.js deleted file mode 100644 index d3133e9..0000000 --- a/app/lib/chrome_tabs.js +++ /dev/null @@ -1,14 +0,0 @@ -function canAccessChromeTabs() { - const canAccess = - typeof chrome !== 'undefined' && typeof chrome.tabs !== 'undefined'; - return canAccess; -} - -function getCurrentTab(callback) { - chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { - const currentTab = tabs[0]; - callback(currentTab); - }); -} - -export { canAccessChromeTabs, getCurrentTab }; diff --git a/app/lib/is_dev.js b/app/lib/is_dev.js deleted file mode 100644 index 482a00d..0000000 --- a/app/lib/is_dev.js +++ /dev/null @@ -1,5 +0,0 @@ -import process from 'process'; - -export default function isDev() { - return process.env.NODE_ENV === 'development'; -} diff --git a/app/lib/notes.js b/app/lib/notes.js deleted file mode 100644 index 0855e88..0000000 --- a/app/lib/notes.js +++ /dev/null @@ -1,82 +0,0 @@ -import { - canAccessStorage, - deleteAllNotesFromStorage, - deleteNotesForUrlFromStorage, - getAllStoredNotesFromStorage, - getNotesForUrlFromStorage, - saveNoteToStorage, -} from './storage/storage.js'; - -// Utility functions - -function defaultNotesForTab(tab) { - const defaultNotes = { - url: tab.url, - title: tab.title, - image: tab.favIconUrl ? tab.favIconUrl : '', - body: '', - }; - return defaultNotes; -} - -// Create / Update functions - -function saveNote(url, details, callback, onError) { - if (canAccessStorage()) { - saveNoteToStorage(url, details, callback, onError); - } else { - if (onError) { - onError(); - } - } -} - -// Read functions - -function getAllStoredNotes(callback) { - if (canAccessStorage()) { - getAllStoredNotesFromStorage(callback); - } else { - // TODO: better error handling? - callback({}); - } -} - -function getNotesForUrl(url, notesFallback, callback) { - if (canAccessStorage()) { - getNotesForUrlFromStorage(url, notesFallback, callback); - } else { - callback(notesFallback); - } -} - -// Delete functions - -function deleteNotesForUrl(url, callback, onError) { - if (canAccessStorage()) { - deleteNotesForUrlFromStorage(url, callback); - } else { - if (onError) { - onError(); - } - } -} - -function deleteAllNotes(callback, onError) { - if (canAccessStorage()) { - deleteAllNotesFromStorage(callback); - } else { - if (onError) { - onError(); - } - } -} - -export { - defaultNotesForTab, - deleteAllNotes, - deleteNotesForUrl, - getAllStoredNotes, - getNotesForUrl, - saveNote, -}; diff --git a/app/lib/storage/chrome_storage.js b/app/lib/storage/chrome_storage.js deleted file mode 100644 index 5a13d9d..0000000 --- a/app/lib/storage/chrome_storage.js +++ /dev/null @@ -1,76 +0,0 @@ -const CHROME_STORAGE_ALL_NOTES_KEY = '__ALL_NOTE_DATA__'; - -function canAccessChromeStorage() { - const canAccess = - typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined'; - return canAccess; -} - -// Create / Update functions - -function saveNoteToChromeStorage(url, details, callback, onError) { - getAllStoredNotesFromChromeStorage((allNotesObject) => { - allNotesObject[url] = details; - - chrome.storage.sync.set( - { [CHROME_STORAGE_ALL_NOTES_KEY]: allNotesObject }, - function () { - callback(allNotesObject); - } - ); - }); -} - -// Read functions - -function getAllStoredNotesFromChromeStorage(callback) { - chrome.storage.sync.get([CHROME_STORAGE_ALL_NOTES_KEY], function (result) { - const allNotesObject = result[CHROME_STORAGE_ALL_NOTES_KEY] || {}; - callback(allNotesObject); - }); -} - -function getNotesForUrlFromChromeStorage(url, notesFallback, callback) { - getAllStoredNotesFromChromeStorage((allNotesObject) => { - const notes = - typeof allNotesObject[url] !== 'undefined' - ? allNotesObject[url] - : notesFallback; - callback(notes); - }); -} - -// Delete functions - -function deleteNotesForUrlFromChromeStorage(url, callback) { - getAllStoredNotesFromChromeStorage((allNotesObject) => { - delete allNotesObject[url]; - - chrome.storage.sync.set( - { [CHROME_STORAGE_ALL_NOTES_KEY]: allNotesObject }, - function () { - callback(allNotesObject); - } - ); - }); -} - -function deleteAllNotesFromChromeStorage(callback) { - chrome.storage.sync.set( - { [CHROME_STORAGE_ALL_NOTES_KEY]: {} }, - function () { - if (callback) { - callback(); - } - } - ); -} - -export { - canAccessChromeStorage, - deleteAllNotesFromChromeStorage, - deleteNotesForUrlFromChromeStorage, - getAllStoredNotesFromChromeStorage, - getNotesForUrlFromChromeStorage, - saveNoteToChromeStorage, -}; diff --git a/app/lib/storage/storage.js b/app/lib/storage/storage.js deleted file mode 100644 index c6166c8..0000000 --- a/app/lib/storage/storage.js +++ /dev/null @@ -1,45 +0,0 @@ -import { - canAccessChromeStorage, - deleteAllNotesFromChromeStorage, - deleteNotesForUrlFromChromeStorage, - getAllStoredNotesFromChromeStorage, - getNotesForUrlFromChromeStorage, - saveNoteToChromeStorage, -} from './chrome_storage.js'; - -function canAccessStorage() { - return canAccessChromeStorage(); -} - -// Create / Update functions - -function saveNoteToStorage(url, details, callback, onError) { - saveNoteToChromeStorage(url, details, callback, onError); -} - -function getAllStoredNotesFromStorage(callback) { - getAllStoredNotesFromChromeStorage(callback); -} - -function getNotesForUrlFromStorage(url, notesFallback, callback) { - getNotesForUrlFromChromeStorage(url, notesFallback, callback); -} - -// Delete functions - -function deleteNotesForUrlFromStorage(url, callback) { - deleteNotesForUrlFromChromeStorage(url, callback); -} - -function deleteAllNotesFromStorage(callback) { - deleteAllNotesFromChromeStorage(callback); -} - -export { - canAccessStorage, - deleteAllNotesFromStorage, - deleteNotesForUrlFromStorage, - getAllStoredNotesFromStorage, - getNotesForUrlFromStorage, - saveNoteToStorage, -}; diff --git a/app/next.config.js b/app/next.config.js deleted file mode 100644 index a73e7a5..0000000 --- a/app/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const isProd = process.env.NODE_ENV === 'production'; - -module.exports = { - basePath: '', - assetPrefix: isProd ? './' : '/', -}; diff --git a/app/package-lock.json b/app/package-lock.json deleted file mode 100644 index fb2f04a..0000000 --- a/app/package-lock.json +++ /dev/null @@ -1,5365 +0,0 @@ -{ - "name": "learn-starter", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@ampproject/toolbox-core": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/@ampproject/toolbox-core/-/toolbox-core-2.7.4.tgz", - "integrity": "sha512-qpBhcS4urB7IKc+jx2kksN7BuvvwCo7Y3IstapWo+EW+COY5EYAUwb2pil37v3TsaqHKgX//NloFP1SKzGZAnw==", - "requires": { - "cross-fetch": "3.0.6", - "lru-cache": "6.0.0" - } - }, - "@ampproject/toolbox-optimizer": { - "version": "2.7.1-alpha.0", - "resolved": "https://registry.npmjs.org/@ampproject/toolbox-optimizer/-/toolbox-optimizer-2.7.1-alpha.0.tgz", - "integrity": "sha512-WGPZKVQvHgNYJk1XVJCCmY+NVGTGJtvn0OALDyiegN4FJWOcilQUhCIcjMkZN59u1flz/u+sEKccM5qsROqVyg==", - "requires": { - "@ampproject/toolbox-core": "^2.7.1-alpha.0", - "@ampproject/toolbox-runtime-version": "^2.7.1-alpha.0", - "@ampproject/toolbox-script-csp": "^2.5.4", - "@ampproject/toolbox-validator-rules": "^2.7.1-alpha.0", - "abort-controller": "3.0.0", - "cross-fetch": "3.0.6", - "cssnano-simple": "1.2.1", - "dom-serializer": "1.1.0", - "domhandler": "3.3.0", - "domutils": "2.4.2", - "htmlparser2": "5.0.1", - "https-proxy-agent": "5.0.0", - "lru-cache": "6.0.0", - "node-fetch": "2.6.1", - "normalize-html-whitespace": "1.0.0", - "postcss": "7.0.32", - "postcss-safe-parser": "4.0.2", - "terser": "5.5.1" - }, - "dependencies": { - "postcss": { - "version": "7.0.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "@ampproject/toolbox-runtime-version": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/@ampproject/toolbox-runtime-version/-/toolbox-runtime-version-2.7.4.tgz", - "integrity": "sha512-SAdOUOERp42thVNWaBJlnFvFVvnacMVnz5z9LyUZHSnoL1EqrAW5Sz5jv+Ly+gkA8NYsEaUxAdSCBAzE9Uzb4Q==", - "requires": { - "@ampproject/toolbox-core": "2.7.4" - } - }, - "@ampproject/toolbox-script-csp": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@ampproject/toolbox-script-csp/-/toolbox-script-csp-2.5.4.tgz", - "integrity": "sha512-+knTYetI5nWllRZ9wFcj7mYxelkiiFVRAAW/hl0ad8EnKHMH82tRlk40CapEnUHhp6Er5sCYkumQ8dngs3Q4zQ==" - }, - "@ampproject/toolbox-validator-rules": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/@ampproject/toolbox-validator-rules/-/toolbox-validator-rules-2.7.4.tgz", - "integrity": "sha512-z3JRcpIZLLdVC9XVe7YTZuB3a/eR9s2SjElYB9AWRdyJyL5Jt7+pGNv4Uwh1uHVoBXsWEVQzNOWSNtrO3mSwZA==", - "requires": { - "cross-fetch": "3.0.6" - } - }, - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/types": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", - "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "@hapi/accept": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-5.0.1.tgz", - "integrity": "sha512-fMr4d7zLzsAXo28PRRQPXR1o2Wmu+6z+VY1UzDp0iFo13Twj8WePakwXBiqn3E1aAlTpSNzCXdnnQXFhst8h8Q==", - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/boom": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.1.tgz", - "integrity": "sha512-VNR8eDbBrOxBgbkddRYIe7+8DZ+vSbV6qlmaN2x7eWjsUjy2VmQgChkOKcVZIeupEZYj+I0dqNg430OhwzagjA==", - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/hoek": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", - "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==" - }, - "@next/env": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-10.0.4.tgz", - "integrity": "sha512-U+XIL75XM1pCmY4+9kYbst/0IptlfDnkFfKdgADBZulQlfng4RB3zirdzkoBtod0lVcrGgDryzOi1mM23RiiVQ==" - }, - "@next/polyfill-module": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@next/polyfill-module/-/polyfill-module-10.0.4.tgz", - "integrity": "sha512-i2gLUa3YuZ2eQg+d91n+jS4YbPVKg1v0HHIUeJFJMMtpG/apBkTuTLBQGJXe4nKNf7/41NWLDft4ihC3Zfd+Yw==" - }, - "@next/react-dev-overlay": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@next/react-dev-overlay/-/react-dev-overlay-10.0.4.tgz", - "integrity": "sha512-8pKN0PspEtfVFqeSpNQymfXWyV95OTIT0xP9IqILJX2+52ICdU5D+YNuNIwpc4ZOZ0CssM/uYsz6K1FHbCaU7A==", - "requires": { - "@babel/code-frame": "7.10.4", - "ally.js": "1.4.1", - "anser": "1.4.9", - "chalk": "4.0.0", - "classnames": "2.2.6", - "data-uri-to-buffer": "3.0.0", - "shell-quote": "1.7.2", - "source-map": "0.8.0-beta.0", - "stacktrace-parser": "0.1.10", - "strip-ansi": "6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "requires": { - "whatwg-url": "^7.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@next/react-refresh-utils": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@next/react-refresh-utils/-/react-refresh-utils-10.0.4.tgz", - "integrity": "sha512-kZ/37aSQaR0GCZVqh7WDLkeEZqzjPQoZUDdo6TOWiIEb+089AmfYp8A4/1ra9Fu8T4b4wnB76TRl6tp6DeJLXg==" - }, - "@opentelemetry/api": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.14.0.tgz", - "integrity": "sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ==", - "requires": { - "@opentelemetry/context-base": "^0.14.0" - } - }, - "@opentelemetry/context-base": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.14.0.tgz", - "integrity": "sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw==" - }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" - }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==" - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==" - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "requires": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" - }, - "adjust-sourcemap-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", - "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==", - "requires": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - } - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" - }, - "ally.js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ally.js/-/ally.js-1.4.1.tgz", - "integrity": "sha1-n7fmuljvrE7pExyymqnuO1QLzx4=", - "requires": { - "css.escape": "^1.5.0", - "platform": "1.3.3" - } - }, - "anser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.9.tgz", - "integrity": "sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA==" - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "arity-n": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", - "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=" - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "ast-types": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.2.tgz", - "integrity": "sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==" - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "optional": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-plugin-transform-define": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-define/-/babel-plugin-transform-define-2.0.0.tgz", - "integrity": "sha512-0dv5RNRUlUKxGYIIErl01lpvi8b7W2R04Qcl1mCj70ahwZcgiklfXnFlh4FGnRh6aayCfSZKdhiMryVzcq5Dmg==", - "requires": { - "lodash": "^4.17.11", - "traverse": "0.6.6" - } - }, - "babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "optional": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.14.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.6.tgz", - "integrity": "sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A==", - "requires": { - "caniuse-lite": "^1.0.30001154", - "electron-to-chromium": "^1.3.585", - "escalade": "^3.1.1", - "node-releases": "^1.1.65" - } - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" - }, - "caniuse-lite": { - "version": "1.0.30001171", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001171.tgz", - "integrity": "sha512-5Alrh8TTYPG9IH4UkRqEBZoEToWRLvPbSQokvzSz0lii8/FOWKG4keO1HoYfPWs8IF/NH/dyNPg1cmJGvV3Zlg==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", - "optional": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", - "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", - "optional": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colorette": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "compose-function": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", - "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=", - "requires": { - "arity-n": "^1.0.4" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "requires": { - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-fetch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", - "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", - "requires": { - "node-fetch": "2.6.1" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "requires": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - } - }, - "css-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", - "integrity": "sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==", - "requires": { - "camelcase": "^6.0.0", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^2.0.0", - "postcss": "^7.0.32", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.3", - "postcss-modules-scope": "^2.2.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^2.7.1", - "semver": "^7.3.2" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - }, - "cssnano-preset-simple": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssnano-preset-simple/-/cssnano-preset-simple-1.2.1.tgz", - "integrity": "sha512-B2KahOIFTV6dw5Ioy9jHshTh/vAYNnUB2enyWRgnAEg3oJBjI/035ExpePaMqS2SwpbH7gCgvQqwpMBH6hTJSw==", - "requires": { - "caniuse-lite": "^1.0.30001093", - "postcss": "^7.0.32" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "cssnano-simple": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssnano-simple/-/cssnano-simple-1.2.1.tgz", - "integrity": "sha512-9vOyjw8Dj/T12kIOnXPZ5VnEIo6F3YMaIn0wqJXmn277R58cWpI3AvtdlCBtohX7VAUNYcyk2d0dKcXXkb5I6Q==", - "requires": { - "cssnano-preset-simple": "1.2.1", - "postcss": "^7.0.32" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "data-uri-to-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.0.tgz", - "integrity": "sha512-MJ6mFTZ+nPQO+39ua/ltwNePXrfdF3Ww0wP1Od7EePySXN1cP9XNqRQOG3FxTfipp8jx898LUCgBCEP11Qw/ZQ==", - "requires": { - "buffer-from": "^1.1.1" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "requires": { - "mimic-response": "^2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "optional": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "dom-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz", - "integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "entities": "^2.0.0" - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "domelementtype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", - "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" - }, - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.2.tgz", - "integrity": "sha512-NKbgaM8ZJOecTZsIzW5gSuplsX2IWW2mIK7xVr8hTQF2v1CJWTmLZ1HOCh5sH+IzVPAGE5IucooOkvwBRAdowA==", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0" - } - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "electron-to-chromium": { - "version": "1.3.633", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz", - "integrity": "sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==" - }, - "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", - "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "requires": { - "prr": "~1.0.1" - } - }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "optional": true - }, - "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "requires": { - "type": "^2.0.0" - }, - "dependencies": { - "type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "optional": true - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "optional": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "htmlparser2": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", - "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - } - }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "requires": { - "postcss": "^7.0.14" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "optional": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "optional": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - }, - "jest-worker": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", - "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^6.1.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "requires": { - "minimist": "^1.2.5" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "klona": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", - "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" - }, - "line-column": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", - "integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=", - "requires": { - "isarray": "^1.0.0", - "isobject": "^2.0.0" - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "optional": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "optional": true - }, - "nanoid": { - "version": "3.1.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", - "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "optional": true - }, - "native-url": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.3.4.tgz", - "integrity": "sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA==", - "requires": { - "querystring": "^0.2.0" - } - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "next": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/next/-/next-10.0.4.tgz", - "integrity": "sha512-WXEYr1FuR2cLuWGN8peYGM6ykmbtwaHvrI6RqR2qrTXUNsW+KU5pzIMK5WPcpqP+xOuMhlykOCJvwJH8qU9FZQ==", - "requires": { - "@ampproject/toolbox-optimizer": "2.7.1-alpha.0", - "@babel/runtime": "7.12.5", - "@hapi/accept": "5.0.1", - "@next/env": "10.0.4", - "@next/polyfill-module": "10.0.4", - "@next/react-dev-overlay": "10.0.4", - "@next/react-refresh-utils": "10.0.4", - "@opentelemetry/api": "0.14.0", - "ast-types": "0.13.2", - "babel-plugin-transform-define": "2.0.0", - "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "browserslist": "4.14.6", - "buffer": "5.6.0", - "caniuse-lite": "^1.0.30001113", - "chalk": "2.4.2", - "chokidar": "3.4.3", - "crypto-browserify": "3.12.0", - "css-loader": "4.3.0", - "cssnano-simple": "1.2.1", - "etag": "1.8.1", - "find-cache-dir": "3.3.1", - "jest-worker": "24.9.0", - "loader-utils": "2.0.0", - "native-url": "0.3.4", - "node-fetch": "2.6.1", - "node-html-parser": "1.4.9", - "p-limit": "3.1.0", - "path-browserify": "1.0.1", - "pnp-webpack-plugin": "1.6.4", - "postcss": "8.1.7", - "process": "0.11.10", - "prop-types": "15.7.2", - "raw-body": "2.4.1", - "react-is": "16.13.1", - "react-refresh": "0.8.3", - "resolve-url-loader": "3.1.2", - "sass-loader": "10.0.5", - "schema-utils": "2.7.1", - "sharp": "0.26.2", - "stream-browserify": "3.0.0", - "style-loader": "1.2.1", - "styled-jsx": "3.3.2", - "use-subscription": "1.5.1", - "vm-browserify": "1.1.2", - "watchpack": "2.0.0-beta.13", - "webpack": "4.44.1", - "webpack-sources": "1.4.3" - } - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node-abi": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", - "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", - "optional": true, - "requires": { - "semver": "^5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "optional": true - } - } - }, - "node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==", - "optional": true - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-html-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.4.9.tgz", - "integrity": "sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw==", - "requires": { - "he": "1.2.0" - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - } - } - }, - "node-releases": { - "version": "1.1.67", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", - "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==" - }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", - "optional": true - }, - "normalize-html-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-html-whitespace/-/normalize-html-whitespace-1.0.0.tgz", - "integrity": "sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "optional": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "pbkdf2": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", - "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "requires": { - "find-up": "^4.0.0" - } - }, - "platform": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.3.tgz", - "integrity": "sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE=" - }, - "pnp-webpack-plugin": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", - "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", - "requires": { - "ts-pnp": "^1.1.6" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "postcss": { - "version": "8.1.7", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.7.tgz", - "integrity": "sha512-llCQW1Pz4MOPwbZLmOddGM9eIJ8Bh7SZ2Oj5sxZva77uVaotYDsYTch1WBTNu7fUY0fpWp0fdt7uW40D4sRiiQ==", - "requires": { - "colorette": "^1.2.1", - "line-column": "^1.0.2", - "nanoid": "^3.1.16", - "source-map": "^0.6.1" - } - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "requires": { - "postcss": "^7.0.5" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", - "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.32", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-safe-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", - "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", - "requires": { - "postcss": "^7.0.26" - }, - "dependencies": { - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "postcss-selector-parser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", - "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "prebuild-install": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", - "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", - "optional": true, - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "simple-get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", - "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "optional": true, - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.3", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - } - }, - "react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "react-refresh": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", - "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "optional": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "resolve-url-loader": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz", - "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==", - "requires": { - "adjust-sourcemap-loader": "3.0.0", - "camelcase": "5.3.1", - "compose-function": "3.0.3", - "convert-source-map": "1.7.0", - "es6-iterator": "2.0.3", - "loader-utils": "1.2.3", - "postcss": "7.0.21", - "rework": "1.0.1", - "rework-visit": "1.0.0", - "source-map": "0.6.1" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "postcss": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", - "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - } - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rework": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", - "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", - "requires": { - "convert-source-map": "^0.3.3", - "css": "^2.0.0" - }, - "dependencies": { - "convert-source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", - "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=" - } - } - }, - "rework-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", - "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=" - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "requires": { - "aproba": "^1.1.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sass-loader": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz", - "integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==", - "requires": { - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "semver": "^7.3.2" - }, - "dependencies": { - "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "requires": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "requires": { - "randombytes": "^2.1.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "optional": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "sharp": { - "version": "0.26.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.26.2.tgz", - "integrity": "sha512-bGBPCxRAvdK9bX5HokqEYma4j/Q5+w8Nrmb2/sfgQCLEUx/HblcpmOfp59obL3+knIKnOhyKmDb4tEOhvFlp6Q==", - "optional": true, - "requires": { - "color": "^3.1.2", - "detect-libc": "^1.0.3", - "node-addon-api": "^3.0.2", - "npmlog": "^4.1.2", - "prebuild-install": "^5.3.5", - "semver": "^7.3.2", - "simple-get": "^4.0.0", - "tar-fs": "^2.1.0", - "tunnel-agent": "^0.6.0" - } - }, - "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "optional": true - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "optional": true - }, - "simple-get": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", - "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", - "optional": true, - "requires": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - }, - "dependencies": { - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "optional": true, - "requires": { - "mimic-response": "^3.1.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "optional": true - } - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "optional": true, - "requires": { - "is-arrayish": "^0.3.1" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "requires": { - "type-fest": "^0.7.1" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "requires": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "optional": true - }, - "style-loader": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz", - "integrity": "sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==", - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^2.6.6" - } - }, - "styled-jsx": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.3.2.tgz", - "integrity": "sha512-daAkGd5mqhbBhLd6jYAjYBa9LpxYCzsgo/f6qzPdFxVB8yoGbhxvzQgkC0pfmCVvW3JuAEBn0UzFLBfkHVZG1g==", - "requires": { - "@babel/types": "7.8.3", - "babel-plugin-syntax-jsx": "6.18.0", - "convert-source-map": "1.7.0", - "loader-utils": "1.2.3", - "source-map": "0.7.3", - "string-hash": "1.1.3", - "stylis": "3.5.4", - "stylis-rule-sheet": "0.0.10" - }, - "dependencies": { - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } - } - }, - "stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" - }, - "stylis-rule-sheet": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "optional": true, - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "tar-stream": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", - "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", - "optional": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "terser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", - "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - } - } - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "requires": { - "setimmediate": "^1.0.4" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" - }, - "ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==" - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "optional": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "use-subscription": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz", - "integrity": "sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==", - "requires": { - "object-assign": "^4.1.1" - } - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" - }, - "watchpack": { - "version": "2.0.0-beta.13", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.0.0-beta.13.tgz", - "integrity": "sha512-ZEFq2mx/k5qgQwgi6NOm+2ImICb8ngAkA/rZ6oyXZ7SgPn3pncf+nfhYTCrs3lmHwOxnPtGLTOuFLfpSMh1VMA==", - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "optional": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "optional": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "optional": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "optional": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "optional": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "optional": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "webpack": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz", - "integrity": "sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==", - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.3.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" - } - } - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "requires": { - "errno": "~0.1.7" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - } - } -} diff --git a/app/package.json b/app/package.json deleted file mode 100644 index 0387e8d..0000000 --- a/app/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "notes-chrome-plugin", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build && next export", - "start": "next start" - }, - "dependencies": { - "bootstrap": "^4.5.3", - "next": "^10.0.0", - "react": "^17.0.1", - "react-bootstrap": "^1.4.0", - "react-dom": "16.13.1", - "sass": "^1.30.0" - } -} diff --git a/app/pages/_app.js b/app/pages/_app.js deleted file mode 100644 index 35ba5a1..0000000 --- a/app/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import '../styles/app.scss'; - -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/app/pages/allnotes.js b/app/pages/allnotes.js deleted file mode 100644 index dd3629f..0000000 --- a/app/pages/allnotes.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Table from 'react-bootstrap/Table'; - -import BasePopup from '../components/base_popup'; -import NoteTableItem from '../components/NoteTableItem'; - -import { deleteNotesForUrl, getAllStoredNotes } from '../lib/notes'; - -import css from '../styles/common.module.scss'; - -const AllNotes = () => { - const [allNotesObject, setAllNotesObject] = useState(null); - - useEffect(() => { - getAllStoredNotes(function (allNotesObj) { - setAllNotesObject(allNotesObj); - }); - }, []); - - const deleteRowFromTable = (noteUrl) => { - deleteNotesForUrl(noteUrl, (allNotesObj) => { - setAllNotesObject(allNotesObj); - }); - }; - - return ( -
- - - - - - - - - - {allNotesObject ? ( - - {Object.keys(allNotesObject).map((urlKey) => ( - - ))} - - ) : ( -

Loading...

- )} -
TitleURLNotesActions
-
- ); -}; - -export default BasePopup(AllNotes); diff --git a/app/pages/options.js b/app/pages/options.js deleted file mode 100644 index e131b8f..0000000 --- a/app/pages/options.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Button } from 'react-bootstrap'; - -import { deleteAllNotes } from '../lib/notes'; - -import BasePage from '../components/base_page'; - -class Options extends React.Component { - constructor(props) { - super(props); - } - - handleDeleteAllClicked() { - // TODO: optionally pass in a callback method to indicate that notes were deleted - deleteAllNotes(); - } - - render() { - return ( -
-

- Warning! -
- If you press this button, all of your saved notes will be - deleted. This is irreversible. -
- -

-

- - Learn more about URL Notes. - -

-
- ); - } -} - -export default BasePage(Options); diff --git a/app/pages/popup.js b/app/pages/popup.js deleted file mode 100644 index 8ba6fe4..0000000 --- a/app/pages/popup.js +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import { Button, Form, Row, Col } from 'react-bootstrap'; -import Head from 'next/head'; - -import { canAccessChromeTabs } from '../lib/chrome_tabs'; -import { saveNote } from '../lib/notes'; - -import BasePopup from '../components/base_popup'; - -class Popup extends React.Component { - constructor(props) { - super(props); - - this.currentTabUrlInput = React.createRef(); - this.currentTabTitleInput = React.createRef(); - this.currentTabIconImg = React.createRef(); - this.currentTabNotesInput = React.createRef(); - } - - saveNotesForCurrentTab() { - const popup = this.props.popup; - const self = this; - - if (popup.state.currentTabUrl === '') { - popup.showErrorAlert(); - } else { - const noteUrl = this.currentTabUrlInput.current.value; - const noteTitle = this.currentTabTitleInput.current.value; - //const noteImage = popup.state.currentTabIcon; - const noteImage = this.currentTabIconImg.current.src; - const noteBody = this.currentTabNotesInput.current.value; - // TODO: Date and Time Properties - - console.log(noteUrl, noteTitle, noteImage, noteBody); - - const noteDetails = { - url: noteUrl, - title: noteTitle, - image: noteImage, - body: noteBody, - }; - - saveNote( - noteUrl, - noteDetails, - (allNotesObject) => { - popup.setState({ allNotesObject }); - popup.showSuccessAlert(); - }, - () => { - popup.showErrorAlert(); - } - ); - } - } - - handleSaveButtonClicked() { - const popup = this.props.popup; - - if (canAccessChromeTabs()) { - this.saveNotesForCurrentTab(); - } else { - popup.showSuccessAlert(); - } - } - - render() { - const popup = this.props.popup; - const currentNotes = popup.state.currentNotes; - - return ( -
-
- - - Tab Image - - - - - Current URL - - - - -
- - Title - - -
-
- - Notes - - -
- -
-
- ); - } -} - -export default BasePopup(Popup); diff --git a/app/public/favicon.ico b/app/public/favicon.ico deleted file mode 100644 index 4965832..0000000 Binary files a/app/public/favicon.ico and /dev/null differ diff --git a/app/public/vercel.svg b/app/public/vercel.svg deleted file mode 100644 index fbf0e25..0000000 --- a/app/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/app/styles/app.scss b/app/styles/app.scss deleted file mode 100644 index 19b0ca9..0000000 --- a/app/styles/app.scss +++ /dev/null @@ -1 +0,0 @@ -@import "~bootstrap/scss/bootstrap"; diff --git a/app/styles/common.module.scss b/app/styles/common.module.scss deleted file mode 100644 index f9c52ba..0000000 --- a/app/styles/common.module.scss +++ /dev/null @@ -1,50 +0,0 @@ -.page { - width: 800px; - box-sizing: border-box; - padding: 0; - margin: 0; -} - -.popup { - width: 800px; - box-sizing: border-box; - padding: 0; - margin: 0; - - #main__add__section { - margin-top: 10px; - margin-bottom: 10px; - padding: 0 !important; - } - - .all__notes__table { - margin-left: 16px; - width: 100%; - - tr { - width: 100%; - } - } - - .fill { - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - - img { - flex-shrink: 0; - min-width: 100%; - min-height: 100%; - } - } - - .nopadding { - padding: 0 !important; - margin: 0 !important; - } -} - -.main { - padding: 20px; -} diff --git a/app/yarn.lock b/app/yarn.lock deleted file mode 100644 index 1a36e7c..0000000 --- a/app/yarn.lock +++ /dev/null @@ -1,4172 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/toolbox-core@2.7.4", "@ampproject/toolbox-core@^2.7.1-alpha.0": - version "2.7.4" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-core/-/toolbox-core-2.7.4.tgz#8355136f16301458ce942acf6c55952c9a415627" - integrity sha512-qpBhcS4urB7IKc+jx2kksN7BuvvwCo7Y3IstapWo+EW+COY5EYAUwb2pil37v3TsaqHKgX//NloFP1SKzGZAnw== - dependencies: - cross-fetch "3.0.6" - lru-cache "6.0.0" - -"@ampproject/toolbox-optimizer@2.7.1-alpha.0": - version "2.7.1-alpha.0" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-optimizer/-/toolbox-optimizer-2.7.1-alpha.0.tgz#1571dcd02608223ff68f6b7223102a123e381197" - integrity sha512-WGPZKVQvHgNYJk1XVJCCmY+NVGTGJtvn0OALDyiegN4FJWOcilQUhCIcjMkZN59u1flz/u+sEKccM5qsROqVyg== - dependencies: - "@ampproject/toolbox-core" "^2.7.1-alpha.0" - "@ampproject/toolbox-runtime-version" "^2.7.1-alpha.0" - "@ampproject/toolbox-script-csp" "^2.5.4" - "@ampproject/toolbox-validator-rules" "^2.7.1-alpha.0" - abort-controller "3.0.0" - cross-fetch "3.0.6" - cssnano-simple "1.2.1" - dom-serializer "1.1.0" - domhandler "3.3.0" - domutils "2.4.2" - htmlparser2 "5.0.1" - https-proxy-agent "5.0.0" - lru-cache "6.0.0" - node-fetch "2.6.1" - normalize-html-whitespace "1.0.0" - postcss "7.0.32" - postcss-safe-parser "4.0.2" - terser "5.5.1" - -"@ampproject/toolbox-runtime-version@^2.7.1-alpha.0": - version "2.7.4" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-runtime-version/-/toolbox-runtime-version-2.7.4.tgz#f49da0dab122101ef75ed3caed3a0142487b73e1" - integrity sha512-SAdOUOERp42thVNWaBJlnFvFVvnacMVnz5z9LyUZHSnoL1EqrAW5Sz5jv+Ly+gkA8NYsEaUxAdSCBAzE9Uzb4Q== - dependencies: - "@ampproject/toolbox-core" "2.7.4" - -"@ampproject/toolbox-script-csp@^2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-script-csp/-/toolbox-script-csp-2.5.4.tgz#d8b7b91a678ae8f263cb36d9b74e441b7d633aad" - integrity sha512-+knTYetI5nWllRZ9wFcj7mYxelkiiFVRAAW/hl0ad8EnKHMH82tRlk40CapEnUHhp6Er5sCYkumQ8dngs3Q4zQ== - -"@ampproject/toolbox-validator-rules@^2.7.1-alpha.0": - version "2.7.4" - resolved "https://registry.yarnpkg.com/@ampproject/toolbox-validator-rules/-/toolbox-validator-rules-2.7.4.tgz#a58b5eca723f6c3557ac83b696de0247f5f03ce4" - integrity sha512-z3JRcpIZLLdVC9XVe7YTZuB3a/eR9s2SjElYB9AWRdyJyL5Jt7+pGNv4Uwh1uHVoBXsWEVQzNOWSNtrO3mSwZA== - dependencies: - cross-fetch "3.0.6" - -"@babel/code-frame@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/runtime@7.12.5", "@babel/runtime@^7.12.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/types@7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" - integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg== - dependencies: - esutils "^2.0.2" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@hapi/accept@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10" - integrity sha512-fMr4d7zLzsAXo28PRRQPXR1o2Wmu+6z+VY1UzDp0iFo13Twj8WePakwXBiqn3E1aAlTpSNzCXdnnQXFhst8h8Q== - dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" - -"@hapi/boom@9.x.x": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.1.tgz#89e6f0e01637c2a4228da0d113e8157c93677b04" - integrity sha512-VNR8eDbBrOxBgbkddRYIe7+8DZ+vSbV6qlmaN2x7eWjsUjy2VmQgChkOKcVZIeupEZYj+I0dqNg430OhwzagjA== - dependencies: - "@hapi/hoek" "9.x.x" - -"@hapi/hoek@9.x.x": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" - integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== - -"@next/env@10.0.4": - version "10.0.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-10.0.4.tgz#ac759094d021853616af56a7bd6720e44a92a303" - integrity sha512-U+XIL75XM1pCmY4+9kYbst/0IptlfDnkFfKdgADBZulQlfng4RB3zirdzkoBtod0lVcrGgDryzOi1mM23RiiVQ== - -"@next/polyfill-module@10.0.4": - version "10.0.4" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-10.0.4.tgz#c34391a12ad80d6e373c403f96c8e2bbd793dca1" - integrity sha512-i2gLUa3YuZ2eQg+d91n+jS4YbPVKg1v0HHIUeJFJMMtpG/apBkTuTLBQGJXe4nKNf7/41NWLDft4ihC3Zfd+Yw== - -"@next/react-dev-overlay@10.0.4": - version "10.0.4" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-10.0.4.tgz#c578a3c71e2f8a8fe2aae8007cc40d1cf10bc768" - integrity sha512-8pKN0PspEtfVFqeSpNQymfXWyV95OTIT0xP9IqILJX2+52ICdU5D+YNuNIwpc4ZOZ0CssM/uYsz6K1FHbCaU7A== - dependencies: - "@babel/code-frame" "7.10.4" - ally.js "1.4.1" - anser "1.4.9" - chalk "4.0.0" - classnames "2.2.6" - data-uri-to-buffer "3.0.0" - shell-quote "1.7.2" - source-map "0.8.0-beta.0" - stacktrace-parser "0.1.10" - strip-ansi "6.0.0" - -"@next/react-refresh-utils@10.0.4": - version "10.0.4" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.0.4.tgz#5ad753572891aa7cb1010b358cc4241d7be20d20" - integrity sha512-kZ/37aSQaR0GCZVqh7WDLkeEZqzjPQoZUDdo6TOWiIEb+089AmfYp8A4/1ra9Fu8T4b4wnB76TRl6tp6DeJLXg== - -"@opentelemetry/api@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.14.0.tgz#4e17d8d2f1da72b19374efa7b6526aa001267cae" - integrity sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ== - dependencies: - "@opentelemetry/context-base" "^0.14.0" - -"@opentelemetry/context-base@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" - integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== - -"@popperjs/core@^2.5.3": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f" - integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw== - -"@restart/context@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02" - integrity sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q== - -"@restart/hooks@^0.3.21", "@restart/hooks@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.3.25.tgz#11004139ad1c70d2f5965a8939dcb5aeb96aa652" - integrity sha512-m2v3N5pxTsIiSH74/sb1yW8D9RxkJidGW+5Mfwn/lHb2QzhZNlaU1su7abSyT9EGf0xS/0waLjrf7/XxQHUk7w== - dependencies: - lodash "^4.17.15" - lodash-es "^4.17.15" - -"@types/classnames@^2.2.10": - version "2.2.11" - resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf" - integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw== - -"@types/invariant@^2.2.33": - version "2.2.34" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe" - integrity sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg== - -"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== - -"@types/prop-types@*", "@types/prop-types@^15.7.3": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== - -"@types/react-transition-group@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" - integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== - dependencies: - "@types/react" "*" - -"@types/react@*": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" - integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/react@^16.9.11", "@types/react@^16.9.35": - version "16.14.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" - integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/warning@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" - integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI= - -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -abort-controller@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -acorn@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -adjust-sourcemap-loader@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" - integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw== - dependencies: - loader-utils "^2.0.0" - regex-parser "^2.2.11" - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ally.js@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/ally.js/-/ally.js-1.4.1.tgz#9fb7e6ba58efac4ee9131cb29aa9ee3b540bcf1e" - integrity sha1-n7fmuljvrE7pExyymqnuO1QLzx4= - dependencies: - css.escape "^1.5.0" - platform "1.3.3" - -anser@1.4.9: - version "1.4.9" - resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" - integrity sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -arity-n@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" - integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U= - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -ast-types@0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" - integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -babel-plugin-syntax-jsx@6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" - integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= - -babel-plugin-transform-define@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-2.0.0.tgz#79c3536635f899aabaf830b194b25519465675a4" - integrity sha512-0dv5RNRUlUKxGYIIErl01lpvi8b7W2R04Qcl1mCj70ahwZcgiklfXnFlh4FGnRh6aayCfSZKdhiMryVzcq5Dmg== - dependencies: - lodash "^4.17.11" - traverse "0.6.6" - -babel-plugin-transform-react-remove-prop-types@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" - integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" - integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - -bootstrap@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6" - integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@4.14.6: - version "4.14.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.6.tgz#97702a9c212e0c6b6afefad913d3a1538e348457" - integrity sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A== - dependencies: - caniuse-lite "^1.0.30001154" - electron-to-chromium "^1.3.585" - escalade "^3.1.1" - node-releases "^1.1.65" - -buffer-from@^1.0.0, buffer-from@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -camelcase@5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - -caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001113, caniuse-lite@^1.0.30001154: - version "1.0.30001171" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001171.tgz#3291e11e02699ad0a29e69b8d407666fc843eba7" - integrity sha512-5Alrh8TTYPG9IH4UkRqEBZoEToWRLvPbSQokvzSz0lii8/FOWKG4keO1HoYfPWs8IF/NH/dyNPg1cmJGvV3Zlg== - -chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" - integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@3.4.3, "chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.1.2" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -classnames@2.2.6, classnames@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" - integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" - -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compose-function@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" - integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8= - dependencies: - arity-n "^1.0.4" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -convert-source-map@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -convert-source-map@^0.3.3: - version "0.3.5" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" - integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-fetch@3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" - integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== - dependencies: - node-fetch "2.6.1" - -crypto-browserify@3.12.0, crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-loader@4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e" - integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg== - dependencies: - camelcase "^6.0.0" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^2.0.0" - postcss "^7.0.32" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.3" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^2.7.1" - semver "^7.3.2" - -css.escape@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" - integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= - -css@^2.0.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== - dependencies: - inherits "^2.0.3" - source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-simple@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-1.2.1.tgz#8976013114b1fc4718253d30f21aaed1780fb80e" - integrity sha512-B2KahOIFTV6dw5Ioy9jHshTh/vAYNnUB2enyWRgnAEg3oJBjI/035ExpePaMqS2SwpbH7gCgvQqwpMBH6hTJSw== - dependencies: - caniuse-lite "^1.0.30001093" - postcss "^7.0.32" - -cssnano-simple@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-1.2.1.tgz#6de5d9dd75774bc8f31767573410a952c7dd8a12" - integrity sha512-9vOyjw8Dj/T12kIOnXPZ5VnEIo6F3YMaIn0wqJXmn277R58cWpI3AvtdlCBtohX7VAUNYcyk2d0dKcXXkb5I6Q== - dependencies: - cssnano-preset-simple "1.2.1" - postcss "^7.0.32" - -csstype@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" - integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== - -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - -data-uri-to-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.0.tgz#8a3088a5efd3f53c3682343313c6895d498eb8d7" - integrity sha512-MJ6mFTZ+nPQO+39ua/ltwNePXrfdF3Ww0wP1Od7EePySXN1cP9XNqRQOG3FxTfipp8jx898LUCgBCEP11Qw/ZQ== - dependencies: - buffer-from "^1.1.1" - -debug@4: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dom-helpers@^5.0.1, dom-helpers@^5.1.2, dom-helpers@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dom-serializer@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.1.0.tgz#5f7c828f1bfc44887dc2a315ab5c45691d544b58" - integrity sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.0.0" - entities "^2.0.0" - -dom-serializer@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" - integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@^2.0.1, domelementtype@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domhandler@3.3.0, domhandler@^3.0.0, domhandler@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" - integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== - dependencies: - domelementtype "^2.0.1" - -domhandler@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" - integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== - dependencies: - domelementtype "^2.1.0" - -domutils@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.2.tgz#7ee5be261944e1ad487d9aa0616720010123922b" - integrity sha512-NKbgaM8ZJOecTZsIzW5gSuplsX2IWW2mIK7xVr8hTQF2v1CJWTmLZ1HOCh5sH+IzVPAGE5IucooOkvwBRAdowA== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^3.3.0" - -domutils@^2.4.2: - version "2.4.4" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" - integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.0.1" - domhandler "^4.0.0" - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -electron-to-chromium@^1.3.585: - version "1.3.633" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" - integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA== - -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" - integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -entities@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - -errno@^0.1.3, errno@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -es5-ext@^0.10.35, es5-ext@^0.10.50: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@2.0.3, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-symbol@^3.1.1, es6-symbol@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== - dependencies: - type "^2.0.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-cache-dir@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" - integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -htmlparser2@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7" - integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.3.0" - domutils "^2.4.2" - entities "^2.0.0" - -http-errors@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -https-proxy-agent@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" - -ieee754@^1.1.13, ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -infer-owner@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -jest-worker@24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" - integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== - dependencies: - merge-stream "^2.0.0" - supports-color "^6.1.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klona@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== - -line-column@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" - integrity sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI= - dependencies: - isarray "^1.0.0" - isobject "^2.0.0" - -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -loader-utils@2.0.0, loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^1.2.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash-es@^4.17.15: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@6.0.0, lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^0.5.1, mkdirp@^0.5.3: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - -nanoid@^3.1.16: - version "3.1.20" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" - integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -native-url@0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" - integrity sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA== - dependencies: - querystring "^0.2.0" - -neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - -next@^10.0.0: - version "10.0.4" - resolved "https://registry.yarnpkg.com/next/-/next-10.0.4.tgz#0d256f58a57d6bab7db7e533900c15f322960b4a" - integrity sha512-WXEYr1FuR2cLuWGN8peYGM6ykmbtwaHvrI6RqR2qrTXUNsW+KU5pzIMK5WPcpqP+xOuMhlykOCJvwJH8qU9FZQ== - dependencies: - "@ampproject/toolbox-optimizer" "2.7.1-alpha.0" - "@babel/runtime" "7.12.5" - "@hapi/accept" "5.0.1" - "@next/env" "10.0.4" - "@next/polyfill-module" "10.0.4" - "@next/react-dev-overlay" "10.0.4" - "@next/react-refresh-utils" "10.0.4" - "@opentelemetry/api" "0.14.0" - ast-types "0.13.2" - babel-plugin-transform-define "2.0.0" - babel-plugin-transform-react-remove-prop-types "0.4.24" - browserslist "4.14.6" - buffer "5.6.0" - caniuse-lite "^1.0.30001113" - chalk "2.4.2" - chokidar "3.4.3" - crypto-browserify "3.12.0" - css-loader "4.3.0" - cssnano-simple "1.2.1" - etag "1.8.1" - find-cache-dir "3.3.1" - jest-worker "24.9.0" - loader-utils "2.0.0" - native-url "0.3.4" - node-fetch "2.6.1" - node-html-parser "1.4.9" - p-limit "3.1.0" - path-browserify "1.0.1" - pnp-webpack-plugin "1.6.4" - postcss "8.1.7" - process "0.11.10" - prop-types "15.7.2" - raw-body "2.4.1" - react-is "16.13.1" - react-refresh "0.8.3" - resolve-url-loader "3.1.2" - sass-loader "10.0.5" - schema-utils "2.7.1" - stream-browserify "3.0.0" - style-loader "1.2.1" - styled-jsx "3.3.2" - use-subscription "1.5.1" - vm-browserify "1.1.2" - watchpack "2.0.0-beta.13" - webpack "4.44.1" - webpack-sources "1.4.3" - optionalDependencies: - sharp "0.26.2" - -node-abi@^2.7.0: - version "2.19.3" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" - integrity sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg== - dependencies: - semver "^5.4.1" - -node-addon-api@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" - integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== - -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-html-parser@1.4.9: - version "1.4.9" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c" - integrity sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw== - dependencies: - he "1.2.0" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^1.1.65: - version "1.1.67" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" - integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== - -noop-logger@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" - integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= - -normalize-html-whitespace@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-html-whitespace/-/normalize-html-whitespace-1.0.0.tgz#5e3c8e192f1b06c3b9eee4b7e7f28854c7601e34" - integrity sha512-9ui7CGtOOlehQu0t/OhhlmDyc71mKVlv+4vF+me4iZLPrNtRL2xoquEdfZxasC/bdQi/Hr3iTrpyRKIG+ocabA== - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npmlog@^4.0.1, npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -p-limit@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-browserify@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -platform@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461" - integrity sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE= - -pnp-webpack-plugin@1.6.4: - version "1.6.4" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" - integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== - dependencies: - ts-pnp "^1.1.6" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" - integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== - dependencies: - icss-utils "^4.1.1" - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== - dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" - -postcss-safe-parser@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" - integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== - dependencies: - postcss "^7.0.26" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@7.0.21: - version "7.0.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" - integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@7.0.32: - version "7.0.32" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" - integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@8.1.7: - version "8.1.7" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.1.7.tgz#ff6a82691bd861f3354fd9b17b2332f88171233f" - integrity sha512-llCQW1Pz4MOPwbZLmOddGM9eIJ8Bh7SZ2Oj5sxZva77uVaotYDsYTch1WBTNu7fUY0fpWp0fdt7uW40D4sRiiQ== - dependencies: - colorette "^1.2.1" - line-column "^1.0.2" - nanoid "^3.1.16" - source-map "^0.6.1" - -postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prebuild-install@^5.3.5: - version "5.3.6" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" - integrity sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@0.11.10, process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -prop-types-extra@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" - integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== - dependencies: - react-is "^16.3.2" - warning "^4.0.0" - -prop-types@15.7.2, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0, querystring@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -raw-body@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== - dependencies: - bytes "3.1.0" - http-errors "1.7.3" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-bootstrap@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-1.4.0.tgz#806a8b48b065cedfb28c6c5c7b0c0e3c3b53445d" - integrity sha512-0BMzgeUAxH126v7VYDzIXbHxQVHSnniPVKpz9fblumdQpWaiElMnnzk+u8h8DoELX0nCXwPlcUzgXqmpncdc2Q== - dependencies: - "@babel/runtime" "^7.4.2" - "@restart/context" "^2.1.4" - "@restart/hooks" "^0.3.21" - "@types/classnames" "^2.2.10" - "@types/invariant" "^2.2.33" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9.35" - "@types/react-transition-group" "^4.4.0" - "@types/warning" "^3.0.0" - classnames "^2.2.6" - dom-helpers "^5.1.2" - invariant "^2.2.4" - prop-types "^15.7.2" - prop-types-extra "^1.1.0" - react-overlays "^4.1.0" - react-transition-group "^4.4.1" - uncontrollable "^7.0.0" - warning "^4.0.3" - -react-dom@16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" - -react-is@16.13.1, react-is@^16.3.2, react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-overlays@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-4.1.1.tgz#0060107cbe1c5171a744ccda3fbf0556d064bc5f" - integrity sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ== - dependencies: - "@babel/runtime" "^7.12.1" - "@popperjs/core" "^2.5.3" - "@restart/hooks" "^0.3.25" - "@types/warning" "^3.0.0" - dom-helpers "^5.2.0" - prop-types "^15.7.2" - uncontrollable "^7.0.0" - warning "^4.0.3" - -react-refresh@0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" - integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== - -react-transition-group@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" - integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" - integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -resolve-url-loader@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08" - integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ== - dependencies: - adjust-sourcemap-loader "3.0.0" - camelcase "5.3.1" - compose-function "3.0.3" - convert-source-map "1.7.0" - es6-iterator "2.0.3" - loader-utils "1.2.3" - postcss "7.0.21" - rework "1.0.1" - rework-visit "1.0.0" - source-map "0.6.1" - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rework-visit@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" - integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo= - -rework@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" - integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc= - dependencies: - convert-source-map "^0.3.3" - css "^2.0.0" - -rimraf@^2.5.4, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-loader@10.0.5: - version "10.0.5" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.5.tgz#f53505b5ddbedf43797470ceb34066ded82bb769" - integrity sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.30.0.tgz#60bbbbaf76ba10117e61c6c24f00161c3d60610e" - integrity sha512-26EUhOXRLaUY7+mWuRFqGeGGNmhB1vblpTENO1Z7mAzzIZeVxZr9EZoaY1kyGLFWdSOZxRMAufiN2mkbO6dAlw== - dependencies: - chokidar ">=2.0.0 <4.0.0" - -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@2.7.1, schema-utils@^2.6.6, schema-utils@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== - dependencies: - "@types/json-schema" "^7.0.6" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -semver@^5.4.1, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== - dependencies: - lru-cache "^6.0.0" - -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -sharp@0.26.2: - version "0.26.2" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.2.tgz#3d5777d246ae32890afe82a783c1cbb98456a88c" - integrity sha512-bGBPCxRAvdK9bX5HokqEYma4j/Q5+w8Nrmb2/sfgQCLEUx/HblcpmOfp59obL3+knIKnOhyKmDb4tEOhvFlp6Q== - dependencies: - color "^3.1.2" - detect-libc "^1.0.3" - node-addon-api "^3.0.2" - npmlog "^4.1.2" - prebuild-install "^5.3.5" - semver "^7.3.2" - simple-get "^4.0.0" - tar-fs "^2.1.0" - tunnel-agent "^0.6.0" - -shell-quote@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - -signal-exit@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-get@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" - integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ== - dependencies: - decompress-response "^6.0.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12, source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@0.7.3, source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -source-map@0.8.0-beta.0: - version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" - integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== - dependencies: - whatwg-url "^7.0.0" - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== - dependencies: - figgy-pudding "^3.5.1" - -stacktrace-parser@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" - integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== - dependencies: - type-fest "^0.7.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.5.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-browserify@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -string-hash@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -style-loader@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" - integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== - dependencies: - loader-utils "^2.0.0" - schema-utils "^2.6.6" - -styled-jsx@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018" - integrity sha512-daAkGd5mqhbBhLd6jYAjYBa9LpxYCzsgo/f6qzPdFxVB8yoGbhxvzQgkC0pfmCVvW3JuAEBn0UzFLBfkHVZG1g== - dependencies: - "@babel/types" "7.8.3" - babel-plugin-syntax-jsx "6.18.0" - convert-source-map "1.7.0" - loader-utils "1.2.3" - source-map "0.7.3" - string-hash "1.1.3" - stylis "3.5.4" - stylis-rule-sheet "0.0.10" - -stylis-rule-sheet@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" - integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== - -stylis@3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" - integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tar-fs@^2.0.0, tar-fs@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" - integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -terser-webpack-plugin@^1.4.3: - version "1.4.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" - integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^4.0.0" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" - integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" - -terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -traverse@0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" - integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= - -ts-pnp@^1.1.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" - integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== - -tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" - integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -uncontrollable@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.1.1.tgz#f67fed3ef93637126571809746323a9db815d556" - integrity sha512-EcPYhot3uWTS3w00R32R2+vS8Vr53tttrvMj/yA1uYRhf8hbTG2GyugGqWDY0qIskxn0uTTojVd6wPYW9ZEf8Q== - dependencies: - "@babel/runtime" "^7.6.3" - "@types/react" "^16.9.11" - invariant "^2.2.4" - react-lifecycles-compat "^3.0.4" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use-subscription@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== - dependencies: - object-assign "^4.1.1" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -vm-browserify@1.1.2, vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -warning@^4.0.0, warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -watchpack-chokidar2@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" - integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== - dependencies: - chokidar "^2.1.8" - -watchpack@2.0.0-beta.13: - version "2.0.0-beta.13" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.0.0-beta.13.tgz#9d9b0c094b8402139333e04eb6194643c8384f55" - integrity sha512-ZEFq2mx/k5qgQwgi6NOm+2ImICb8ngAkA/rZ6oyXZ7SgPn3pncf+nfhYTCrs3lmHwOxnPtGLTOuFLfpSMh1VMA== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -watchpack@^1.7.4: - version "1.7.5" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" - integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.1" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -webpack-sources@1.4.3, webpack-sources@^1.4.0, webpack-sources@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@4.44.1: - version "4.44.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" - integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.3.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/backgroundScriptOnInstall.js b/backgroundScriptOnInstall.js deleted file mode 100644 index 3fde60b..0000000 --- a/backgroundScriptOnInstall.js +++ /dev/null @@ -1,6 +0,0 @@ -chrome.runtime.onInstalled.addListener(function () { - chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { - let url = tabs[0].url; - console.log(url); - }); -}); diff --git a/img/get_started128.png b/img/get_started128.png deleted file mode 100644 index 4c1cf87..0000000 Binary files a/img/get_started128.png and /dev/null differ diff --git a/img/get_started16.png b/img/get_started16.png deleted file mode 100644 index fb8531c..0000000 Binary files a/img/get_started16.png and /dev/null differ diff --git a/img/get_started32.png b/img/get_started32.png deleted file mode 100644 index 7715223..0000000 Binary files a/img/get_started32.png and /dev/null differ diff --git a/img/get_started48.png b/img/get_started48.png deleted file mode 100644 index 94ddde9..0000000 Binary files a/img/get_started48.png and /dev/null differ diff --git a/img/icon128.png b/img/icon128.png new file mode 100644 index 0000000..88da22b Binary files /dev/null and b/img/icon128.png differ diff --git a/img/icon16.png b/img/icon16.png new file mode 100644 index 0000000..1bc5dfb Binary files /dev/null and b/img/icon16.png differ diff --git a/img/icon32.png b/img/icon32.png new file mode 100644 index 0000000..880c445 Binary files /dev/null and b/img/icon32.png differ diff --git a/img/icon48.png b/img/icon48.png new file mode 100644 index 0000000..e8a8287 Binary files /dev/null and b/img/icon48.png differ diff --git a/manifest.json b/manifest.json index 8cfc529..bec8358 100644 --- a/manifest.json +++ b/manifest.json @@ -1,50 +1,25 @@ { - "manifest_version": 2, - - "name": "URL Notes", - "version": "0.1.0", - - "author": "Hacktoolkit (@hacktoolkit)", - - "description": "Chrome extension to allow users to easily take and store notes on URLs directly on the browser.", - - "browser_action": { - "default_icon": { - "16": "img/get_started16.png", - "32": "img/get_started32.png", - "48": "img/get_started48.png", - "128": "img/get_started128.png" + "manifest_version": 3, + "name": "Marginalia (Page Notes)", + "short_name": "Marginalia", + "version": "1.0.0", + "description": "Notes that follow the page, stickies pinned to its parts, a local record of what it looked like, reminders, Markdown export.", + "author": "Hacktoolkit (@hacktoolkit)", + "minimum_chrome_version": "121", + "icons": { "16": "img/icon16.png", "32": "img/icon32.png", "48": "img/icon48.png", "128": "img/icon128.png" }, + "action": { + "default_icon": { "16": "img/icon16.png", "32": "img/icon32.png" }, + "default_popup": "src/popup.html", + "default_title": "Marginalia (Page Notes)" }, - "default_popup": "dist/popup.html", - "default_title": "URL Notes" - }, - - "options_ui": { - "page": "dist/options.html", - "chrome_style": true - }, - - "background": { - "scripts": [ - "backgroundScriptOnInstall.js" - ], - "persistent": false - }, - - "permissions": [ - "declarativeContent", - "storage", - "tabs" - ], - - "commands": { - "_execute_browser_action": { - "suggested_key": { - "default": "Ctrl+Shift+N", - "mac": "MacCtrl+Shift+N" - }, - "description": "Opens the extension popup with the following key shortcut." + "options_ui": { "page": "src/options.html", "open_in_tab": true }, + "side_panel": { "default_path": "src/panel.html" }, + "web_accessible_resources": [{ "resources": ["src/content/layer.js", "src/lib/anchor.js", "src/panel.html", "img/icon32.png"], "matches": [""] }], + "optional_host_permissions": ["https://*/*", "http://*/*"], + "background": { "service_worker": "src/background.js", "type": "module" }, + "permissions": ["tabs", "storage", "unlimitedStorage", "alarms", "notifications", "contextMenus", "activeTab", "scripting", "sidePanel"], + "commands": { + "_execute_action": { "suggested_key": { "default": "Alt+Shift+N" }, "description": "Open Marginalia for this page" }, + "snoozeTab": { "description": "Snooze this tab until tomorrow morning" } } - }, - "content_security_policy": "script-src 'self' https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js; object-src 'self'" } diff --git a/package.json b/package.json new file mode 100644 index 0000000..edba03c --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "marginalia-chrome-extension", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { "test": "node --test" } +} diff --git a/promo/note-detail.png b/promo/note-detail.png new file mode 100644 index 0000000..d96bf6a Binary files /dev/null and b/promo/note-detail.png differ diff --git a/promo/options.png b/promo/options.png new file mode 100644 index 0000000..d0d0142 Binary files /dev/null and b/promo/options.png differ diff --git a/promo/popup-dark.png b/promo/popup-dark.png new file mode 100644 index 0000000..a391393 Binary files /dev/null and b/promo/popup-dark.png differ diff --git a/promo/popup-remind.png b/promo/popup-remind.png new file mode 100644 index 0000000..48760a0 Binary files /dev/null and b/promo/popup-remind.png differ diff --git a/promo/popup.png b/promo/popup.png new file mode 100644 index 0000000..efb6246 Binary files /dev/null and b/promo/popup.png differ diff --git a/promo/stickies-page.png b/promo/stickies-page.png new file mode 100644 index 0000000..670c45c Binary files /dev/null and b/promo/stickies-page.png differ diff --git a/promo/stickies-pick.png b/promo/stickies-pick.png new file mode 100644 index 0000000..1566f10 Binary files /dev/null and b/promo/stickies-pick.png differ diff --git a/promo/store/1-popup.png b/promo/store/1-popup.png new file mode 100644 index 0000000..79d2c53 Binary files /dev/null and b/promo/store/1-popup.png differ diff --git a/promo/store/2-insights.png b/promo/store/2-insights.png new file mode 100644 index 0000000..599a874 Binary files /dev/null and b/promo/store/2-insights.png differ diff --git a/promo/store/3-remind.png b/promo/store/3-remind.png new file mode 100644 index 0000000..97f91cc Binary files /dev/null and b/promo/store/3-remind.png differ diff --git a/promo/store/4-notes.png b/promo/store/4-notes.png new file mode 100644 index 0000000..0481047 Binary files /dev/null and b/promo/store/4-notes.png differ diff --git a/promo/store/description.txt b/promo/store/description.txt new file mode 100644 index 0000000..530f265 --- /dev/null +++ b/promo/store/description.txt @@ -0,0 +1,24 @@ +Marginalia is for people who think while they browse. Write a note on any page and it is there the next time you land on that page, even when the URL is different. + +A NOTE THAT FOLLOWS THE PAGE +A pull request and its Files tab. A Jira issue and its board link. A LinkedIn profile and its Experience page. A Google Doc in edit and view mode. One note, with dated entries, and a count on the toolbar icon so you know the page has one. + +A LOCAL RECORD OF WHAT YOU SAW +Every time you save an entry or open the popup on a noted page, Marginalia keeps what the page looked like: title, metadata and text, stored only in your browser. Pages change and people edit their profiles; your captures do not. When a page differs from your last visit, the popup says so and shows what changed. + +PROFILE INSIGHTS +On a profile page, the capture is read into a work history, and Marginalia points out what a careful reader would ask about: two full-time roles that overlap, two current roles, dates that run backwards, short stints, long gaps, a stated "10+ years" the visible history does not reach, and anything that changed since your last visit. It summarises years, employers and average tenure and builds a one-line card grouped by employer, ready to paste. These are questions to ask, never verdicts. + +REMIND ME, SNOOZE THAT TAB +Remind me about this page in an hour, tonight, tomorrow, next week or at a time. Snooze a tab: close it now, get it back later. + +CAPTURE A QUOTE +Select text, right-click, add it to the page's note. + +YOUR NOTES ARE FILES +Every note is Markdown with YAML frontmatter that Obsidian reads natively. Download them all as a zip, with every capture alongside as plain text, import them back, or connect an Obsidian vault folder and have every save written straight into it. + +PRIVATE BY CONSTRUCTION +No accounts, no server, no analytics, no network requests. Nothing runs on pages you merely visit; a page is only read when you act on it. Open source under the MIT license. + +Source: https://github.com/hacktoolkit/notes-chrome-extension diff --git a/scripts/dev.mjs b/scripts/dev.mjs new file mode 100644 index 0000000..b6c03f4 --- /dev/null +++ b/scripts/dev.mjs @@ -0,0 +1,37 @@ +// Launches the holodeck: a throwaway browser profile at ~/.holodeck/ +// with this folder loaded as an unpacked extension and a few sample tabs. +// +// make dev # Brave by default +// BROWSER=/path/to/chromium make dev +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { basename, dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const EXT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const CANDIDATES = [ + process.env.BROWSER, + '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + '/usr/bin/brave-browser' +].filter(Boolean); +const CHROME = CANDIDATES.find((c) => existsSync(c)); +if (!CHROME) { + console.error('No Chromium-based browser found. Set BROWSER=/path/to/binary (Google Chrome ignores --load-extension).'); + process.exit(2); +} +const HOLODECK = process.env.HOLODECK || join(homedir(), '.holodeck'); +const slug = basename(CHROME).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-browser$/, ''); +const PROFILE = join(HOLODECK, slug); +const prefs = join(PROFILE, 'Default', 'Preferences'); +if (!existsSync(prefs)) { + mkdirSync(dirname(prefs), { recursive: true }); + writeFileSync(prefs, JSON.stringify({ profile: { name: '🧪 holodeck' } })); +} +const URLS = ['https://example.com/', 'https://example.com/about', 'https://example.org/', 'chrome://newtab/', 'chrome://newtab/']; +const child = spawn(CHROME, [`--user-data-dir=${PROFILE}`, `--load-extension=${EXT}`, '--no-first-run', '--no-default-browser-check', ...URLS], { detached: true, stdio: 'ignore' }); +child.unref(); +console.log(`Launched ${CHROME}\nHolodeck profile: ${PROFILE}\nReload the extension at chrome://extensions after editing source.`); diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs new file mode 100644 index 0000000..98266fc --- /dev/null +++ b/scripts/e2e.mjs @@ -0,0 +1,768 @@ +// End-to-end smoke test. Launches a headless Chromium-based browser with the +// extension loaded, drives it over the DevTools protocol, and asserts on real +// browser state. Network is blocked so seeded URLs stay exactly as given. +// +// make e2e +// BROWSER=/path/to/chromium make e2e +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join as pjoin, extname } from 'node:path'; +import { createServer } from 'node:http'; + +// A local page for the in-page layer, plus the extension's src served over +// http so the layer module can be imported into the page with a stubbed +// chrome.* (the real injection needs a click the harness cannot give). +const FIXTURE_HTML = `Pricing – Site +
+

Welcome to the product.

+

Pricing

Plans start at $9 per month, billed yearly.

+
  • First point about speed
  • Second point about pricing
  • Third point about support
+
`; +const server = createServer((req, res) => { + const url = new URL(req.url, 'http://127.0.0.1'); + if (url.pathname === '/pricing') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(FIXTURE_HTML); } + if (url.pathname.startsWith('/src/')) { + try { + const body = readFileSync(pjoin(EXT, url.pathname), 'utf8'); + res.writeHead(200, { 'content-type': extname(url.pathname) === '.js' ? 'text/javascript' : 'text/plain' }); + return res.end(body); + } catch (e) { /* fall through */ } + } + res.writeHead(404); res.end('nope'); +}); +await new Promise((r) => server.listen(0, '127.0.0.1', r)); +const LOCAL = `http://127.0.0.1:${server.address().port}`; +import { homedir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const EXT = process.env.EXT || resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const NAME = JSON.parse(readFileSync(join(EXT, 'manifest.json'), 'utf8')).name; +const CANDIDATES = [process.env.BROWSER, '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/brave-browser'].filter(Boolean); +const CHROME = CANDIDATES.find((c) => existsSync(c)); +if (!CHROME) { console.error('No Chromium-based browser found. Set BROWSER=/path/to/binary'); process.exit(2); } +const PORT = Number(process.env.PORT) || 9337; +const HOLODECK = process.env.HOLODECK || join(homedir(), '.holodeck'); +mkdirSync(join(HOLODECK, 'tmp'), { recursive: true }); +const profile = mkdtempSync(join(HOLODECK, 'tmp', 'e2e-')); + +const chrome = spawn(CHROME, [ + `--user-data-dir=${profile}`, `--load-extension=${EXT}`, `--remote-debugging-port=${PORT}`, + '--headless=new', '--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1', '--no-first-run', '--no-default-browser-check', + ...(process.env.CI ? ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : []), + 'about:blank' +], { stdio: ['ignore', 'ignore', 'pipe'] }); +let stderr = ''; +chrome.stderr.on('data', (d) => { stderr += d; }); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const targets = async () => (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); +async function waitFor(pred, label, tries = 60) { + for (let i = 0; i < tries; i++) { + try { const t = (await targets()).find(pred); if (t) return t; } catch (e) { /* not up yet */ } + await sleep(250); + } + throw new Error(`timeout waiting for ${label}`); +} +class CDP { + constructor(ws) { + this.ws = ws; this.id = 0; this.pending = new Map(); + ws.addEventListener('message', (m) => { + const msg = JSON.parse(m.data); + if (msg.id && this.pending.has(msg.id)) { const { res, rej } = this.pending.get(msg.id); this.pending.delete(msg.id); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); } + }); + } + static async connect(url) { const ws = new WebSocket(url); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; }); return new CDP(ws); } + send(method, params = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((res, rej) => this.pending.set(id, { res, rej })); } + async eval(expression) { + const r = await this.send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); + if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || JSON.stringify(r.exceptionDetails)); + return r.result.value; + } +} +// Browsers ship built-in extensions with service workers of their own, so find +// ours by manifest name rather than by URL shape. +async function findExtensionId() { + for (let i = 0; i < 60; i++) { + let list = []; + try { list = await targets(); } catch (e) { /* not up yet */ } + for (const t of list.filter((t) => t.type === 'service_worker' && t.url.endsWith('/src/background.js'))) { + try { + const cdp = await CDP.connect(t.webSocketDebuggerUrl); + const name = await cdp.eval('chrome.runtime.getManifest().name'); + cdp.ws.close(); + if (name === NAME) return new URL(t.url).host; + } catch (e) { /* try the next one */ } + } + await sleep(250); + } + throw new Error(`timeout waiting for the ${NAME} service worker\n${stderr.split('\n').slice(-5).join('\n')}`); +} + +let failures = 0; +function check(label, cond, detail = '') { + console.log(`${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ` ${detail}` : ''}`); + if (!cond) failures++; +} + +async function screenshot(cdp, name, width, height, dark = false) { + if (!process.env.E2E_SCREENSHOTS) return; + mkdirSync(process.env.E2E_SCREENSHOTS, { recursive: true }); + await cdp.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false }); + await cdp.send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: dark ? 'dark' : 'light' }] }); + await sleep(200); + const shot = await cdp.send('Page.captureScreenshot', { format: 'png' }); + writeFileSync(join(process.env.E2E_SCREENSHOTS, `${name}.png`), Buffer.from(shot.data, 'base64')); +} + +try { + const extId = await findExtensionId(); + const browserInfo = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json(); + const browser = await CDP.connect(browserInfo.webSocketDebuggerUrl); + + // the popup opened as a tab can drive chrome.runtime.sendMessage + await browser.send('Target.createTarget', { url: `chrome-extension://${extId}/src/popup.html`, newWindow: true }); + const popupT = await waitFor((t) => t.url.endsWith('/src/popup.html'), 'popup'); + const popup = await CDP.connect(popupT.webSocketDebuggerUrl); + for (let i = 0; i < 40; i++) { + if (await popup.eval(`typeof chrome !== 'undefined' && !!chrome.windows && location.protocol === 'chrome-extension:'`).catch(() => false)) break; + await sleep(250); + } + const toMarkdownOf = (id) => popup.eval(`import('./lib/notes.js').then(async (m) => m.toMarkdown(await new Promise((res) => chrome.runtime.sendMessage({ type: 'get', args: { id: ${JSON.stringify(id)} } }, (r) => res(r.result)))))`); + const send = (msg) => popup.eval(`new Promise((res, rej) => chrome.runtime.sendMessage(${JSON.stringify(msg)}, (r) => r && r.ok ? res(r.result) : rej(new Error(r ? r.error : chrome.runtime.lastError?.message))))`); + const tabs = () => popup.eval('chrome.tabs.query({})'); + + await popup.eval(`chrome.windows.create({ url: ['https://github.com/dataro/app/pull/482', 'https://github.com/dataro/app/pull/482/files', 'https://example.com/plain'], focused: false })`); + await sleep(1200); + const tabByUrl = async (u) => (await tabs()).find((t) => t.url === u); + const pr = await tabByUrl('https://github.com/dataro/app/pull/482'); + const prFiles = await tabByUrl('https://github.com/dataro/app/pull/482/files'); + const plain = await tabByUrl('https://example.com/plain'); + + // current page with no note yet + let cur = await send({ type: 'current', args: { tabId: pr.id } }); + check('current resolves the page and its rule', cur.tab?.id === pr.id && cur.ident?.rule === 'GitHub pull request' && cur.note === null, JSON.stringify(cur.ident)); + + // add two entries from two URL variants of the same page + const n1 = await send({ type: 'addEntry', args: { tabId: pr.id, text: 'Reviewed, waiting on CI.' } }); + const n2 = await send({ type: 'addEntry', args: { tabId: prFiles.id, text: 'CI green, two nits.' } }); + check('note follows the page across URL variants', n1.id === n2.id && n2.entries.length === 2, `${n1.id} vs ${n2.id}, ${n2.entries.length} entries`); + check('entries are dated', /^\d{4}-\d{2}-\d{2}T/.test(n2.entries[0].at)); + + // badge shows the entry count on both variant tabs + await sleep(600); + const badgeA = await popup.eval(`chrome.action.getBadgeText({ tabId: ${pr.id} })`); + const badgeB = await popup.eval(`chrome.action.getBadgeText({ tabId: ${prFiles.id} })`); + const badgeC = await popup.eval(`chrome.action.getBadgeText({ tabId: ${plain.id} })`); + check('presence badge on both variants, none on an unrelated page', badgeA === '2' && badgeB === '2' && badgeC === '', `${badgeA}/${badgeB}/${badgeC}`); + check('saved-note badge is red and explains its count', (await popup.eval(`chrome.action.getBadgeBackgroundColor({ tabId: ${pr.id} })`)).slice(0, 3).join(',') === '199,59,59' && /2 saved notes/.test(await popup.eval(`chrome.action.getTitle({ tabId: ${pr.id} })`))); + + // edit and remove + const edited = await send({ type: 'updateEntry', args: { id: n2.id, index: 0, text: 'Reviewed. Waiting on CI.' } }); + check('entry edit', edited.entries[0].text === 'Reviewed. Waiting on CI.' && edited.entries[0].edited); + const removed = await send({ type: 'removeEntry', args: { id: n2.id, index: 1 } }); + check('entry remove', removed.entries.length === 1); + + // list and search + let rows = await send({ type: 'list' }); + check('list shows the note', rows.length === 1 && rows[0].entries === 1, `${rows.length} rows`); + rows = await send({ type: 'list', args: { query: 'waiting ci' } }); + check('search matches entry text', rows.length === 1); + rows = await send({ type: 'list', args: { query: 'zebra' } }); + check('search excludes non-matches', rows.length === 0); + + // reminder creates an alarm + const withRem = await send({ type: 'setReminder', args: { id: n2.id, preset: '1h' } }); + const alarm = await popup.eval(`chrome.alarms.get('remind:${n2.id}')`); + check('reminder sets an alarm', withRem.remind && alarm && Math.abs(alarm.scheduledTime - new Date(withRem.remind).getTime()) < 2000, JSON.stringify(alarm)); + const rems = await send({ type: 'reminders' }); + check('reminders list', rems.length === 1 && rems[0].id === n2.id); + await send({ type: 'setReminder', args: { id: n2.id, when: null } }); + check('reminder cleared', !(await popup.eval(`chrome.alarms.get('remind:${n2.id}')`))); + + // snooze closes the tab and schedules it + const before = (await tabs()).length; + const snoozed = await send({ type: 'snoozeTab', args: { tabId: plain.id, preset: '3h', text: 'come back to this' } }); + await sleep(400); + const after = (await tabs()).length; + const plainNote = (await send({ type: 'list', args: { query: 'come back' } }))[0]; + check('snooze closes the tab and creates a reminder', after === before - 1 && snoozed.when && plainNote && plainNote.remind === new Date(snoozed.when).toISOString(), `${before}->${after}`); + // snoozing without text still leaves an entry, so the note survives the reminder firing + await popup.eval(`chrome.windows.create({ url: ['https://snooze.test/quiet'], focused: false })`); + await sleep(600); + const quietTab = (await tabs()).find((t) => /snooze\.test\/quiet/.test(t.url)); + await send({ type: 'snoozeTab', args: { tabId: quietTab.id, preset: '1h' } }); + const quiet = (await send({ type: 'list', args: { query: 'snoozed until' } }))[0]; + check('snooze without text records an entry', quiet && quiet.entries === 1, JSON.stringify(quiet)); + await send({ type: 'deleteNote', args: { id: quiet.id } }); + + // export and import round-trip + const exp = await send({ type: 'export' }); + check('export zips every note', exp.count === 2 && exp.files >= exp.count && exp.base64.length > 100, `${exp.count} notes, ${exp.files} files`); + const md = await popup.eval(`(async () => { const m = await import('./lib/notes.js'); const s = await import('./lib/store.js'); const n = await s.getNote('${n2.id}'); return m.toMarkdown(n); })()`); + check('markdown has frontmatter and a dated heading', /^---\ntitle: /.test(md) && /## \d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(md)); + await send({ type: 'deleteNote', args: { id: n2.id } }); + check('delete removes the note', (await send({ type: 'list' })).length === 1); + const imp = await send({ type: 'import', args: { files: [{ name: 'x.md', text: md }, { name: 'bad.md', text: 'nope' }] } }); + check('import restores from markdown and reports bad files', imp.imported === 1 && imp.errors.length === 1, JSON.stringify(imp)); + const restored = await send({ type: 'get', args: { id: n2.id } }); + check('restored note has its entry', restored && restored.entries.length === 1 && restored.entries[0].text === 'Reviewed. Waiting on CI.'); + // importing the same file again over the existing note must not duplicate entries + const again = await send({ type: 'import', args: { files: [{ name: 'x.md', text: md }] } }); + const afterAgain = await send({ type: 'get', args: { id: n2.id } }); + check('re-import merges without duplicating entries', again.merged === 1 && afterAgain.entries.length === 1, `${afterAgain.entries.length} entries`); + // a past reminder in an imported file does not arm an alarm + const pastMd = md.replace(/^updated: .*$/m, (l) => `${l}\nremind: 2020-01-01T09:00:00.000Z`); + await send({ type: 'import', args: { files: [{ name: 'x.md', text: pastMd }] } }); + check('past reminder from import is not armed', !(await popup.eval(`chrome.alarms.get('remind:${n2.id}')`))); + + // profile intelligence: captures are supplied directly (activeTab needs a + // real click), parsed, analysed, and diffed across visits + const fixture = readFileSync(pjoin(EXT, 'test', 'fixtures', 'linkedin-overlap.txt'), 'utf8'); + await popup.eval(`chrome.windows.create({ url: ['http://www.linkedin.com/in/jane-doe/'], focused: false })`); + await sleep(800); + // HSTS preload may upgrade the scheme; match on host and path only + const liTab = (await tabs()).find((t) => /linkedin\.com\/in\/jane-doe/.test(t.url)); + const liNote = await send({ type: 'addEntry', args: { tabId: liTab.id, title: 'Jane Doe | LinkedIn', text: 'Screening for the CSE role.', snapshot: { title: 'Jane Doe | LinkedIn', text: fixture, meta: { siteName: 'LinkedIn' } } } }); + check('linkedin capture is parsed into a structured snapshot', liNote.snapshots?.length === 1 && liNote.rule === 'LinkedIn profile', JSON.stringify({ snaps: liNote.snapshots?.length, rule: liNote.rule })); + let ins = await send({ type: 'insights', args: { id: liNote.id } }); + check('insights analyse experience', ins.kind === 'linkedin-profile' && ins.summary?.employers === 4 && ins.findings.some((f) => f.code === 'overlap'), JSON.stringify({ kind: ins.kind, employers: ins.summary?.employers, codes: ins.findings?.map((f) => f.code) })); + check('card line is built', /Acme Corp: Head of Things, 2023–present; Globex: Engineer → Senior Engineer/.test(ins.summary.card), ins.summary.card); + // second visit with an edited profile: opening the popup captures again + const editedFixture = fixture.replace('Jan 2021 - Feb 2025 · 4 yrs 2 mos', 'Jan 2021 - Feb 2023 · 2 yrs 2 mos'); + const cur2 = await send({ type: 'current', args: { tabId: liTab.id, snapshot: { title: 'Jane Doe | LinkedIn', text: editedFixture } } }); + check('popup open adds a capture', cur2.note.snapshots.length === 2 && cur2.insights.captures === 2, `${cur2.note.snapshots.length} captures`); + check('edits between visits are detected', cur2.insights.edits?.some((e) => e.code === 'dates' && /were Jan 2021–Feb 2025, now Jan 2021–Feb 2023/.test(e.text)), JSON.stringify(cur2.insights.edits)); + check('plain text diff is reported too', cur2.insights.changed && cur2.insights.changed.addedCount === 1 && cur2.insights.changed.removedCount === 1, JSON.stringify(cur2.insights.changed)); + // an identical capture within the hour changes nothing + const cur3 = await send({ type: 'current', args: { tabId: liTab.id, snapshot: { title: 'Jane Doe | LinkedIn', text: editedFixture } } }); + check('identical capture does not grow history', cur3.note.snapshots.length === 2 && cur3.note.snapshots[1].seen === cur3.note.snapshots[1].at); + const detail = await send({ type: 'detail', args: { id: liNote.id } }); + check('detail returns full snapshots with text', detail.note.snapshots[0].text.length > 500 && detail.insights.profile.experience.length === 5); + // the popup shows the insights + await popup.eval(`chrome.tabs.update(${liTab.id}, { active: true })`); + await popup.eval(`chrome.windows.update(${liTab.windowId}, { focused: true })`); + await popup.eval('location.reload()'); + await sleep(1200); + const insRows = await popup.eval(`[...document.querySelectorAll('#insights .ins')].map((r) => r.className.replace('ins ', '')).join(',')`); + // the edited profile no longer overlaps, so there is no ask row; edit and summary remain + check('popup renders edit and summary rows', /edit/.test(insRows) && /summary/.test(insRows), insRows); + + // capture-only note: keeps the page without an entry, survives in the index + await popup.eval(`chrome.windows.create({ url: ['https://capture.test/page'], focused: false })`); + await sleep(600); + const capTab = (await tabs()).find((t) => /capture\.test\/page/.test(t.url)); + const capOnly = await send({ type: 'capture', args: { tabId: capTab.id, snapshot: { title: 'Capture', text: 'line one\nline two' } } }); + check('capture without an entry creates a note with history', capOnly.note && capOnly.note.entries.length === 0 && capOnly.note.snapshots.length === 1, JSON.stringify({ entries: capOnly.note?.entries?.length, snaps: capOnly.note?.snapshots?.length })); + check('capture-only note is hidden from the default list', !(await send({ type: 'list' })).some((r) => r.id === capOnly.note.id)); + check('capture-only note is listed when asked', (await send({ type: 'list', args: { includeCaptures: true } })).some((r) => r.id === capOnly.note.id)); + + // autosave: the editor writes today's entry, same day edits it + await popup.eval(`chrome.windows.create({ url: ['https://draft.test/page'], focused: false })`); + await sleep(600); + const draftTab = (await tabs()).find((t) => /draft\.test\/page/.test(t.url)); + let d = await send({ type: 'draft', args: { tabId: draftTab.id, text: 'typing' } }); + check('draft creates today\'s entry', d.note && d.note.entries.length === 1 && d.today === 0, JSON.stringify({ entries: d.note?.entries?.length, today: d.today })); + d = await send({ type: 'draft', args: { tabId: draftTab.id, text: 'typing more' } }); + check('draft updates today\'s entry in place', d.note.entries.length === 1 && d.note.entries[0].text === 'typing more'); + d = await send({ type: 'draft', args: { tabId: draftTab.id, text: '' } }); + check('emptying the editor removes today\'s entry', !d.note || d.note.entries.length === 0); + d = await send({ type: 'draft', args: { tabId: draftTab.id, text: 'back again' } }); + await popup.eval(`chrome.tabs.update(${draftTab.id}, { active: true })`); + await popup.eval(`chrome.windows.update(${draftTab.windowId}, { focused: true })`); + await popup.eval('location.reload()'); + await sleep(1200); + const editorValue = await popup.eval(`document.querySelector('#entry').value`); + check('popup editor shows today\'s entry on open', editorValue === 'back again', editorValue); + const hasSave = await popup.eval(`!!document.querySelector('#save')`); + check('no Save button', !hasSave); + // typing in the editor autosaves and says so + await popup.eval(`const t = document.querySelector('#entry'); t.value = 'back again, typed'; t.dispatchEvent(new Event('input'));`); + await sleep(1200); + const status = await popup.eval(`document.querySelector('#status').textContent`); + const typed = await send({ type: 'get', args: { id: d.note.id } }); + check('typing autosaves today\'s entry', /^Saved /.test(status) && typed.entries[0].text === 'back again, typed', `${status} | ${typed.entries[0]?.text}`); + + // popup renders the current note + await popup.eval(`chrome.tabs.update(${pr.id}, { active: true })`); + await popup.eval(`chrome.windows.update(${pr.windowId}, { focused: true })`); + await popup.eval('location.reload()'); + await sleep(1200); + // the PR note's only entry was written today, so it lives in the editor, not the list + const rendered = await popup.eval(`document.querySelector('#entry').value + '|' + document.querySelectorAll('#entries .entry').length`); + check('popup shows today\'s entry in the editor for the active page', rendered === 'Reviewed. Waiting on CI.|0', rendered); + + // stickies: anchored notes through the message API + await popup.eval(`chrome.windows.create({ url: ['${LOCAL}/pricing'], focused: false })`); + await sleep(800); + const priceTab = (await tabs()).find((t) => t.url === `${LOCAL}/pricing`); + const anchorA = { css: '#pricing > p:nth-of-type(1)', path: 'body > div:nth-of-type(1) > main:nth-of-type(1) > section:nth-of-type(1) > p:nth-of-type(1)', quote: { exact: 'Plans start at $9 per month, billed yearly.', prefix: 'Pricing ', suffix: '' }, tag: 'p', label: '' }; + const st1 = await send({ type: 'stickyAdd', args: { url: priceTab.url, title: 'Pricing – Site', anchor: anchorA, text: 'Lead with the annual price.' } }); + check('stickyAdd creates a sticky on a new note', st1 && st1.id && st1.text === 'Lead with the annual price.', JSON.stringify(st1)); + await send({ type: 'stickyUpdate', args: { url: priceTab.url, id: st1.id, text: 'Lead with the annual price, then monthly.' } }); + const st2 = await send({ type: 'stickyAdd', args: { url: priceTab.url, anchor: { ...anchorA, css: '#pricing > h2:nth-of-type(1)', quote: { exact: 'Pricing', prefix: '', suffix: '' }, tag: 'h2' } } }); + let listed = await send({ type: 'stickies', args: { url: priceTab.url } }); + check('stickies list and update', listed.length === 2 && listed[0].text === 'Lead with the annual price, then monthly.' && listed[1].text === '', JSON.stringify(listed.map((s) => s.text))); + const priceRow = (await send({ type: 'list' })).find((r) => r.url === priceTab.url); + check('a sticky-only note is listed with its sticky count', priceRow && priceRow.stickies === 2, JSON.stringify(priceRow)); + await sleep(500); + check('toolbar counts sticky-only notes', await popup.eval(`chrome.action.getBadgeText({ tabId: ${priceTab.id} })`) === '2'); + check('stickies are visible by default', (await send({ type: 'stickyVisibility' })).visible === true); + await send({ type: 'stickyVisibility', args: { visible: false } }); + check('sticky visibility is saved globally', (await send({ type: 'stickyVisibility' })).visible === false && (await popup.eval(`chrome.storage.local.get('stickies:visible')`))['stickies:visible'] === false); + await send({ type: 'stickyVisibility', args: { visible: true } }); + const fb = await send({ type: 'feedback', args: { id: priceRow.id } }); + check('feedback export is a numbered list with quotes', /^Feedback on Pricing – Site\n/.test(fb) && /1\. "Plans start at \$9 per month, billed yearly\."\n Lead with the annual price, then monthly\.\n2\. "Pricing"$/.test(fb), fb); + await send({ type: 'stickyRemove', args: { url: priceTab.url, id: st2.id } }); + listed = await send({ type: 'stickies', args: { url: priceTab.url } }); + check('stickyRemove', listed.length === 1); + + // links: pages connected across sites, symmetric, by canonical key + const linkedA = await send({ type: 'link', args: { id: n2.id, otherId: liNote.id } }); + check('link connects two pages and resolves the other side', linkedA.note.links.length === 1 && linkedA.linked.length === 1 && linkedA.linked[0].id === liNote.id && linkedA.linked[0].host === 'linkedin.com', JSON.stringify(linkedA.linked)); + const liLinks = await send({ type: 'links', args: { id: liNote.id } }); + check('a link is visible from both pages', liLinks.length === 1 && liLinks[0].id === n2.id && /github/.test(liLinks[0].host), JSON.stringify(liLinks)); + check('linking again is a no-op', (await send({ type: 'link', args: { id: n2.id, otherId: liNote.id } })).note.links.length === 1); + const selfLink = await send({ type: 'link', args: { id: n2.id, otherId: n2.id } }).then(() => '', (e) => e.message); + check('a page cannot link to itself', /itself/.test(selfLink), selfLink); + check('markdown export carries links by key', /\nlinks: \[linkedin:/.test(await toMarkdownOf(n2.id)), (await toMarkdownOf(n2.id)).split('\n').find((l) => l.startsWith('links:')) || 'no links line'); + const n2Row = (await send({ type: 'list' })).find((r) => r.id === n2.id); + check('index rows count links and resolve them to titles and sites', n2Row.links === 1 && n2Row.linked?.[0]?.host === 'linkedin.com' && /LinkedIn/.test(n2Row.linked[0].title), JSON.stringify(n2Row.linked)); + const linkedByUrl = await send({ type: 'link', args: { url: 'https://example.com/never-noted', title: 'Fresh page', otherId: liNote.id } }); + check('linking a page without a note creates one', linkedByUrl.note.entries.length === 0 && linkedByUrl.linked[0].id === liNote.id && (await send({ type: 'links', args: { id: liNote.id } })).length === 2, JSON.stringify(linkedByUrl.note.links)); + await send({ type: 'deleteNote', args: { id: linkedByUrl.note.id } }); + check('deleting a page removes it from the pages it was linked to', (await send({ type: 'links', args: { id: liNote.id } })).length === 1); + const unlinked = await send({ type: 'unlink', args: { id: liNote.id, otherId: n2.id } }); + check('unlink removes both sides', unlinked.linked.length === 0 && (await send({ type: 'links', args: { id: n2.id } })).length === 0); + await send({ type: 'link', args: { id: n2.id, otherId: liNote.id } }); // kept for the UI checks below + + // two-way vault through the in-memory folder seam: another profile's file + // comes in, local notes go out, a deletion elsewhere propagates + const otherMd = ['---', 'title: From another profile', 'url: https://docs.example.com/guide', 'key: https://docs.example.com/guide', 'created: 2026-10-10T09:00:00.000Z', 'updated: 2026-10-10T09:05:00.000Z', 'app: marginalia', '---', '', '## 2026-10-10 09:05', '', 'Written in profile B.', ''].join('\n'); + let vault = await send({ type: 'syncVault', args: { folder: { files: { 'from-another-profile-x.md': { text: otherMd, mtime: 1000 } } } } }); + const guideRow = (await send({ type: 'list' })).find((r) => r.url === 'https://docs.example.com/guide'); + check('vault sync imports a file written by another profile', vault.status === 'ok' && vault.report.imported === 1 && guideRow && guideRow.entries === 1, JSON.stringify(vault.report)); + check('vault sync writes this profile\'s notes and renames the file to its canonical name', vault.report.written >= 5 && Object.keys(vault.folder).some((f) => f.endsWith(`-${n2.id}.md`)) && !vault.folder['from-another-profile-x.md'] && Object.keys(vault.folder).some((f) => f.endsWith(`-${guideRow.id}.md`)), Object.keys(vault.folder).join(',')); + check('vault files carry links by key', /\nlinks: \[linkedin:/.test(vault.folder[Object.keys(vault.folder).find((f) => f.endsWith(`-${n2.id}.md`))].text)); + const steady = await send({ type: 'syncVault', args: { folder: { files: vault.folder }, state: vault.state } }); + check('a second sync over an unchanged folder reads and writes nothing', steady.report.read === 0 && steady.report.written === 0 && steady.report.imported === 0, JSON.stringify(steady.report)); + delete vault.folder[Object.keys(vault.folder).find((f) => f.endsWith(`-${guideRow.id}.md`))]; + const afterDelete = await send({ type: 'syncVault', args: { folder: { files: vault.folder }, state: vault.state } }); + check('a file deleted by another profile deletes the note here', afterDelete.report.deletedLocal === 1 && !(await send({ type: 'list' })).some((r) => r.id === guideRow.id), JSON.stringify(afterDelete.report)); + const vs = await send({ type: 'vaultStatus' }); + check('vault status records the last sync', vs.result === 'ok' && vs.at && vs.report && vs.connected === false, JSON.stringify({ result: vs.result, connected: vs.connected })); + check('the real folder is not connected in this profile', (await send({ type: 'syncVault' })).status === 'no-vault'); + check('sync alarm is armed for this profile', !!(await popup.eval(`chrome.alarms.get('vault:sync')`))); + + // event-driven: a watcher names the changed files and the sync is scoped to them + const scopedFolder = { ...vault.folder }; + const n2File = Object.keys(scopedFolder).find((f) => f.endsWith(`-${n2.id}.md`)); + scopedFolder[n2File] = { text: scopedFolder[n2File].text.replace(/\n## /, '\n## 2026-01-05 12:00\n\nLanded from another profile.\n\n## '), mtime: 999999 }; + const scoped = await send({ type: 'syncVault', args: { folder: { files: scopedFolder }, state: afterDelete.state, only: [n2File] } }); + check('a scoped sync reads only the reported file and merges it', scoped.report.scoped === true && scoped.report.scanned === 1 && scoped.report.read === 1 && scoped.report.updated === 1 && (await send({ type: 'get', args: { id: n2.id } })).entries.some((e) => e.text === 'Landed from another profile.'), JSON.stringify(scoped.report)); + check('pages can watch the folder; the worker reports honestly that it cannot', await popup.eval(`import('./lib/vault.js').then((v) => v.canObserve())`) === true && (await send({ type: 'vaultStatus' })).workerWatching === false); + // the real observer, over the origin-private file system (no picker needed): the record shape we rely on + const observed = await popup.eval(`(async () => { + const v = await import('./lib/vault.js'); + if (!v.canObserve()) return 'unsupported'; + const dir = await navigator.storage.getDirectory(); + const seen = []; + const observer = await v.observeHandle(dir, (c) => seen.push({ names: [...c.names], full: c.full })); + const fh = await dir.getFileHandle('watched-note.md', { create: true }); + const w = await fh.createWritable(); await w.write('---\\ntitle: x\\n---\\n'); await w.close(); + const other = await dir.getFileHandle('ignored.txt', { create: true }); const w2 = await other.createWritable(); await w2.write('x'); await w2.close(); + for (let i = 0; i < 40 && !seen.some((c) => c.names.includes('watched-note.md')); i++) await new Promise((r) => setTimeout(r, 100)); + observer.disconnect(); + await dir.removeEntry('watched-note.md'); await dir.removeEntry('ignored.txt'); + return JSON.stringify(seen); + })()`); + check('FileSystemObserver reports changed Markdown files by name and ignores other files', observed !== 'unsupported' && JSON.parse(observed).some((c) => c.names.includes('watched-note.md')) && !JSON.parse(observed).some((c) => c.names.includes('ignored.txt')), observed); + + // sharing stickies as a flat file: compressed, optionally encrypted, merged on import by page identity + const priceNoteId = (await send({ type: 'list' })).find((r) => r.url === priceTab.url).id; + const shared = await send({ type: 'shareExport', args: { id: priceNoteId, passphrase: 'open sesame' } }); + check('shareExport produces a wrapped encrypted text file', /^-----BEGIN MARGINALIA-----\nmarginalia\/1 deflate aes-gcm\n/.test(shared.text) && shared.text.trimEnd().endsWith('-----END MARGINALIA-----') && shared.fileName.endsWith('.marginalia.txt') && shared.stickies === 1, shared.fileName); + const wrongPass = await send({ type: 'shareImport', args: { text: shared.text, passphrase: 'nope' } }).then(() => '', (e) => e.message); + check('the wrong passphrase is refused', /Wrong passphrase/.test(wrongPass), wrongPass); + // simulate the recipient: wipe the sticky locally, then import + const beforeImport = await send({ type: 'get', args: { id: priceNoteId } }); + await send({ type: 'stickyRemove', args: { url: priceTab.url, id: beforeImport.stickies[0].id } }); + const imported = await send({ type: 'shareImport', args: { text: shared.text, passphrase: 'open sesame', url: priceTab.url } }); + check('shareImport restores the stickies onto the recipient\'s note for that page', imported.addedStickies === 1 && imported.samePage === true && (await send({ type: 'stickies', args: { url: priceTab.url } })).length === 1, JSON.stringify(imported)); + check('importing the same share again adds nothing', (await send({ type: 'shareImport', args: { text: shared.text, passphrase: 'open sesame' } })).addedStickies === 0); + const plainShare = await send({ type: 'shareExport', args: { id: priceNoteId, includeEntries: true } }); + check('an unencrypted share with entries decodes without a passphrase', /deflate none/.test(plainShare.text) && (await send({ type: 'shareImport', args: { text: plainShare.text } })).addedStickies === 0); + const junk = await send({ type: 'shareImport', args: { text: 'hello there' } }).then(() => '', (e) => e.message); + check('foreign text is rejected', /not a Marginalia share/.test(junk), junk); + const settingsTab = await popup.eval(`chrome.tabs.create({ url: 'chrome://version/', active: false })`); + await sleep(500); + const annotateErr = await send({ type: 'annotate', args: { tabId: settingsTab.id, pick: true } }).then(() => '', (e) => e.message); + check('annotate refuses non-web pages', /web pages only/.test(annotateErr), annotateErr); + await popup.eval(`chrome.tabs.remove(${settingsTab.id})`); + + // the in-page layer, in a real DOM: load the module over http with a stubbed chrome + const pageT = await waitFor((t) => t.url === `${LOCAL}/pricing`, 'pricing page'); + const page = await CDP.connect(pageT.webSocketDebuggerUrl); + check('fixture page loaded from the local server', (await page.eval('document.title')) === 'Pricing – Site', await page.eval('document.title')); + await page.eval(` + window.__store = { stickies: ${JSON.stringify(listed)}, calls: [], visible: true }; + window.chrome = { runtime: { + id: 'stub', + getManifest: () => ({ name: 'Marginalia (Page Notes)', short_name: 'Marginalia', author: 'Hacktoolkit (@hacktoolkit)' }), + getURL: (p) => '${LOCAL}/' + p, + onMessage: { addListener(fn) { window.__onMessage = fn; } }, + sendMessage(msg, cb) { + window.__store.calls.push(msg); + const a = msg.args || {}; + if (msg.type === 'stickyVisibility') { if (typeof a.visible === 'boolean') window.__store.visible = a.visible; return cb({ ok: true, result: { visible: window.__store.visible } }); } + if (msg.type === 'stickies') return cb({ ok: true, result: window.__store.stickies }); + if (msg.type === 'stickyAdd') { const s = { id: 's' + Date.now(), anchor: a.anchor, text: '', created: '', updated: '' }; window.__store.stickies.push(s); return cb({ ok: true, result: s }); } + if (msg.type === 'stickyUpdate') { const s = window.__store.stickies.find((x) => x.id === a.id); if (s) s.text = a.text; return cb({ ok: true, result: true }); } + if (msg.type === 'stickyRemove') { window.__store.stickies = window.__store.stickies.filter((x) => x.id !== a.id); return cb({ ok: true, result: true }); } + if (msg.type === 'widgetLayout') { window.__store.layout = { ...(window.__store.layout || {}), ...(a.layout || {}) }; return cb({ ok: true, result: window.__store.layout }); } + cb({ ok: true, result: true }); + } + } }; + import('${LOCAL}/src/content/layer.js').then((m) => { window.__layer = m.boot(); return 'booted'; }) + `); + await sleep(800); + const shadowQ = (sel) => `[...document.querySelector('[data-page-notes]').shadowRoot.querySelectorAll(${JSON.stringify(sel)})]`; + const pins = await page.eval(`${shadowQ('.pin')}.map((p) => p.textContent + ':' + p.className).join(',')`); + check('layer renders a pin for the resolved sticky', pins === '1:pin', pins); + const pinPos = await page.eval(`(() => { const p = ${shadowQ('.pin')}[0]; const r = document.querySelector('#pricing > p').getBoundingClientRect(); return Math.abs(parseFloat(p.style.left) - Math.min(r.left + r.width - 14, window.innerWidth - 30)) < 2 && Math.abs(parseFloat(p.style.top) - Math.max(0, r.top - 14)) < 2; })()`); + check('pin sits at the element\'s top-right corner', pinPos === true); + // click the pin: popover opens with the sticky text; typing autosaves through sendMessage + await page.eval(`${shadowQ('.pin')}[0].click()`); + await sleep(200); + const popText = await page.eval(`${shadowQ('.pop textarea')}[0]?.value`); + check('clicking a pin opens its popover', popText === 'Lead with the annual price, then monthly.', popText); + await page.eval(`const ta = ${shadowQ('.pop textarea')}[0]; ta.value = 'Edited in place.'; ta.dispatchEvent(new Event('input'));`); + await sleep(700); + const saved = await page.eval(`window.__store.calls.filter((c) => c.type === 'stickyUpdate').pop()?.args.text`); + check('popover edits autosave', saved === 'Edited in place.', String(saved)); + await screenshot(page, 'sticky', 1000, 700); + await page.eval(`${shadowQ('.pop textarea')}[0].value = 'Saved while hiding'; ${shadowQ('.dock button')}[0].click();`); + await sleep(300); + check('hide all conceals annotations and flushes the open sticky', await page.eval(`${shadowQ('.annotations')}[0].hidden && !${shadowQ('.dock')}[0].hidden && window.__store.stickies[0].text === 'Saved while hiding' && !window.__store.visible`)); + await page.eval(`window.__layer.refresh(); true`); + await sleep(200); + check('hidden stickies stay hidden after refresh', await page.eval(`${shadowQ('.annotations')}[0].hidden`)); + await page.eval(`${shadowQ('.dock button')}[0].click();`); + await sleep(100); + check('show all restores sticky pins', await page.eval(`!${shadowQ('.annotations')}[0].hidden && window.__store.visible`)); + await page.eval(`window.__onMessage({ type: 'pn:visibility', visible: false }, {}, () => {});`); + check('visibility broadcasts hide another open layer', await page.eval(`${shadowQ('.annotations')}[0].hidden`)); + // pick mode: hover the list item, click it, a new sticky is created with a quote anchor + await page.eval(`window.__layer.enterPick(); true`); + await sleep(100); + const picked = await page.eval(`(() => { + const li = document.querySelectorAll('#pricing li')[1]; + const r = li.getBoundingClientRect(); + document.dispatchEvent(new MouseEvent('mousemove', { clientX: r.left + 5, clientY: r.top + 5, bubbles: true })); + document.dispatchEvent(new MouseEvent('click', { clientX: r.left + 5, clientY: r.top + 5, bubbles: true })); + return new Promise((res) => setTimeout(() => res(window.__store.calls.filter((c) => c.type === 'stickyAdd').pop()?.args.anchor), 400)); + })()`); + check('pick mode anchors the clicked element', picked && picked.quote.exact === 'Second point about pricing' && /li:nth-of-type\(2\)$/.test(picked.css), JSON.stringify(picked)); + await sleep(400); + const pinsAfter = await page.eval(`${shadowQ('.pin')}.length`); + check('new sticky gets a pin', pinsAfter === 2, String(pinsAfter)); + // an anchor whose text is gone shows as an orphan + await page.eval(`window.__store.stickies.push({ id: 'ghost', anchor: { css: '#nowhere', path: 'body > p:nth-of-type(9)', quote: { exact: 'Text that is not on this page at all', prefix: '', suffix: '' }, tag: 'p' }, text: 'lost' }); window.__layer.refresh(); true`); + await sleep(400); + const orphan = await page.eval(`${shadowQ('.pin.orphan')}.length + ':' + (window.__store.calls.filter((c) => c.type === 'stickyPlacement').pop()?.args.placement.find((p) => p.id === 'ghost')?.found)`); + check('unresolvable sticky is marked orphan and reported', orphan === '1:false', orphan); + + // a layer orphaned by an extension reload is replaced, not reused, by the bootstrap + const rebooted = await page.eval(`(async () => { + const hosts = () => document.querySelectorAll('[data-page-notes]').length; + window.__pageNotesLayer = { refresh() { window.__staleRefreshed = true; }, alive: () => false }; + const before = hosts(); + const src = await (await fetch('${LOCAL}/src/content/annotate.js')).text(); + (0, eval)(src); + await new Promise((r) => setTimeout(r, 600)); + return JSON.stringify({ before, after: hosts(), stale: !!window.__staleRefreshed, alive: window.__pageNotesLayer?.alive?.() }); + })()`); + check('bootstrap replaces a dead layer and keeps one host', rebooted === '{"before":1,"after":1,"stale":false,"alive":true}', rebooted); + await page.eval(`window.__layer = window.__pageNotesLayer; true`); + await sleep(300); + check('the fresh layer renders the stickies again', await page.eval(`${shadowQ('.pin')}.length === 3`), String(await page.eval(`${shadowQ('.pin')}.length`))); + const kept = await page.eval(`(async () => { const h = document.querySelector('[data-page-notes]'); const src = await (await fetch('${LOCAL}/src/content/annotate.js')).text(); (0, eval)(src); await new Promise((r) => setTimeout(r, 200)); return document.querySelector('[data-page-notes]') === h; })()`); + check('a live layer is reused by the bootstrap', kept === true); + + // floating launch mode: the worker's click handler asks the layer for the widget + await page.send('Emulation.setDeviceMetricsOverride', { width: 1000, height: 760, deviceScaleFactor: 1, mobile: false }); + check('no floating button before the toolbar is used in floating mode', await page.eval(`${shadowQ('.fab')}[0].hidden && ${shadowQ('.widget')}.length === 0`)); + await page.eval(`window.__store.layout = { fab: { x: 30, y: 40 } }; window.__onMessage({ type: 'pn:widget', toggle: true }, {}, () => {});`); + await sleep(300); + check('toolbar click shows the floating button where it was left and opens the widget', await page.eval(`!${shadowQ('.fab')}[0].hidden && ${shadowQ('.fab')}[0].style.left === '30px' && ${shadowQ('.fab')}[0].style.top === '40px' && ${shadowQ('.widget')}.length === 1 && !${shadowQ('.widget')}[0].hidden`), await page.eval(`${shadowQ('.fab')}[0].style.cssText`)); + check('widget bar reads Marginalia by Hacktoolkit', (await page.eval(`${shadowQ('.widget .bar')}[0].textContent.replace(/\\s+/g, ' ').trim()`)).includes('Marginalia by Hacktoolkit'), await page.eval(`${shadowQ('.widget .bar')}[0].textContent`)); + check('widget hosts the extension\'s own panel page', (await page.eval(`${shadowQ('.widget iframe')}[0].getAttribute('src')`)) === `${LOCAL}/src/panel.html?embed=1`); + check('widget opens beside the button', await page.eval(`(() => { const f = ${shadowQ('.fab')}[0].getBoundingClientRect(); const w = ${shadowQ('.widget')}[0].getBoundingClientRect(); return Math.abs(w.left - f.left) < 2 && w.top >= f.bottom; })()`)); + await page.eval(`window.__onMessage({ type: 'pn:widget', toggle: true }, {}, () => {});`); + await sleep(100); + check('a second toolbar click closes the widget and keeps the button', await page.eval(`${shadowQ('.widget')}[0].hidden && !${shadowQ('.fab')}[0].hidden`)); + // drag the button: a short press is a click (opens), a real drag moves and is remembered + const drag = (sel, dx, dy) => page.eval(`(() => { const el = ${shadowQ(sel)}[0]; const r = el.getBoundingClientRect(); const x = r.left + 10, y = r.top + 10; const ev = (type, cx, cy) => el.dispatchEvent(new PointerEvent(type, { pointerId: 1, button: 0, clientX: cx, clientY: cy, bubbles: true })); ev('pointerdown', x, y); ev('pointermove', x + ${dx}, y + ${dy}); ev('pointerup', x + ${dx}, y + ${dy}); return [el.style.left, el.style.top].join(','); })()`); + const tapped = await drag('.fab', 1, 1); + await sleep(100); + check('a tap on the floating button opens the widget', await page.eval(`!${shadowQ('.widget')}[0].hidden`) && tapped === '30px,40px', tapped); + const dragged = await drag('.fab', 120, 60); + await sleep(100); + check('dragging the floating button moves it and remembers the spot', dragged === '150px,100px' && await page.eval(`window.__store.layout.fab.x === 150 && window.__store.layout.fab.y === 100 && !${shadowQ('.widget')}[0].hidden`), `${dragged} ${JSON.stringify(await page.eval('window.__store.layout'))}`); + await screenshot(page, 'widget', 1000, 760); + const draggedWidget = await drag('.widget .bar', 50, 30); + check('the widget drags by its bar and remembers the spot', await page.eval(`window.__store.layout.widget && Math.abs(window.__store.layout.widget.x - parseFloat(${shadowQ('.widget')}[0].style.left)) < 1`), `${draggedWidget} ${JSON.stringify(await page.eval('window.__store.layout.widget'))}`); + await page.eval(`${shadowQ('.widget .bar button')}[0].click()`); + check('the widget\'s own close control hides it', await page.eval(`${shadowQ('.widget')}[0].hidden`)); + await page.eval(`window.__layer.openWidget(); true`); + await page.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`); + check('Esc on the page closes the widget', await page.eval(`${shadowQ('.widget')}[0].hidden`)); + + // side panel: same view, follows the active tab, setting flips the icon behaviour + check('popup offers the sidebar', await popup.eval(`!!document.querySelector('#open-panel')`)); + // on a browser page the view says there is nothing to note + const versionTab = await popup.eval(`chrome.tabs.create({ url: 'chrome://version/', active: false })`); + await sleep(800); + const emptyState = await send({ type: 'current', args: { tabId: versionTab.id } }); + check('browser pages are reported but not notable', /^chrome:/.test(emptyState.tab?.url || '') && emptyState.note === null, emptyState.tab?.url); + await popup.eval(`chrome.tabs.remove(${versionTab.id})`); + // the panel lives in one window and follows that window's active tab + await popup.eval(`chrome.tabs.create({ windowId: ${liTab.windowId}, url: 'chrome-extension://${extId}/src/panel.html', active: false })`); + const panelT = await waitFor((t) => t.url.endsWith('/src/panel.html'), 'panel'); + const panel = await CDP.connect(panelT.webSocketDebuggerUrl); + await panel.send('Emulation.setDeviceMetricsOverride', { width: 390, height: 860, deviceScaleFactor: 1, mobile: false }); + await sleep(1200); + // the panel refreshes asynchronously after a tab switch; poll rather than guess a delay + const panelTitleIs = async (re) => { for (let i = 0; i < 40; i++) { const t = await panel.eval(`document.querySelector('#page-title').textContent`); if (re.test(t)) return t; await sleep(150); } return panel.eval(`document.querySelector('#page-title').textContent`); }; + await popup.eval(`chrome.tabs.update(${liTab.id}, { active: true })`); + const panelTitle2 = await panelTitleIs(/linkedin/i); + await popup.eval(`chrome.tabs.move(${pr.id}, { windowId: ${liTab.windowId}, index: -1 }).then(() => chrome.tabs.update(${pr.id}, { active: true }))`); + const panelTitle1 = await panelTitleIs(/github/); + await popup.eval(`chrome.tabs.update(${liTab.id}, { active: true })`); + await panelTitleIs(/linkedin/i); + check('side panel follows the active tab', /github/.test(panelTitle1) && /LinkedIn|linkedin/i.test(panelTitle2), `${panelTitle1} -> ${panelTitle2}`); + const panelEditor = await panel.eval(`document.querySelector('#entry').value`); + check('side panel shows today\'s entry for the active tab', panelEditor === 'Screening for the CSE role.', panelEditor); + check('sidebar flags previously saved notes', await panel.eval(`!document.querySelector('#saved-here').hidden && /1 saved note/.test(document.querySelector('#saved-here').textContent)`)); + check('empty sticky hint flows across the card', await panel.eval(`getComputedStyle(document.querySelector('#sticky-list .empty-row')).display === 'block' && document.querySelector('#sticky-list .empty-row').getBoundingClientRect().height < 60`)); + + // Retained activeTab permission must not turn automatic panel refreshes + // into captures. Supply a snapshot on every lookup to simulate access. + await panel.eval(` + window.__panelSend = chrome.runtime.sendMessage.bind(chrome.runtime); + window.__panelCalls = []; + chrome.runtime.sendMessage = (msg, cb) => { + window.__panelCalls.push(msg); + if (msg.type === 'current') msg = { ...msg, args: { ...msg.args, snapshot: { title: 'Unexpected passive capture', text: 'This should never be captured by the panel.' } } }; + if (msg.type === 'draft' && window.__failDraft) { + window.__failDraft = false; + cb({ ok: false, error: 'Simulated save failure' }); + return; + } + const hold = msg.type === 'draft' && window.__holdDraft; + if (hold) window.__holdDraft = false; + const holdCurrent = msg.type === 'current' && window.__holdCurrent; + if (holdCurrent) window.__holdCurrent = false; + window.__panelSend(msg, (r) => { + if (hold) window.__releaseDraft = () => cb(r); + else if (holdCurrent) window.__releaseCurrent = () => cb(r); + else cb(r); + }); + }; + `); + const panelWait = async (expression) => { + for (let i = 0; i < 60; i++) { + if (await panel.eval(expression)) return true; + await sleep(100); + } + return false; + }; + await popup.eval(`chrome.tabs.move(${draftTab.id}, { windowId: ${liTab.windowId}, index: -1 }).then(() => chrome.tabs.update(${draftTab.id}, { active: true }))`); + check('panel loads the draft page without a passive capture', await panelWait(`document.querySelector('#entry').value === 'back again, typed' && document.querySelector('#page-url').textContent === 'draft.test/page'`)); + const panelCaptures = await send({ type: 'get', args: { id: d.note.id } }); + check('panel lookup leaves capture history untouched', !panelCaptures.snapshots?.length && await panel.eval(`window.__panelCalls.some((m) => m.type === 'current') && window.__panelCalls.filter((m) => m.type === 'current').every((m) => m.args.capture === false)`)); + + // Failed text stays dirty and can be retried without another keystroke. + await panel.eval(`window.__failDraft = true; document.querySelector('#entry').value = 'Retry this draft'; document.querySelector('#entry').dispatchEvent(new Event('blur'));`); + check('autosave reports a failed write', await panelWait(`document.querySelector('#status').textContent === 'Simulated save failure'`)); + await panel.eval(`document.querySelector('#entry').dispatchEvent(new Event('blur'));`); + check('unchanged text retries successfully after a failed save', await panelWait(`document.querySelector('#status').textContent.startsWith('Saved ')`) && (await send({ type: 'get', args: { id: d.note.id } })).entries[0].text === 'Retry this draft'); + + // Hold a save response while the user types more and navigates the same + // tab. Both drafts belong to the displayed page, even after its tab moves. + await panel.eval(`window.__holdDraft = true; document.querySelector('#entry').value = 'First pending draft'; document.querySelector('#entry').dispatchEvent(new Event('blur'));`); + check('draft save can remain in flight', await panelWait(`typeof window.__releaseDraft === 'function'`)); + await panel.eval(`document.querySelector('#entry').value = 'Last draft before navigation'; document.querySelector('#entry').dispatchEvent(new Event('input'));`); + await popup.eval(`chrome.tabs.update(${draftTab.id}, { url: 'https://draft.test/next' })`); + await sleep(200); + await panel.eval(`window.__releaseDraft();`); + check('panel follows navigation after draining pending edits', await panelWait(`document.querySelector('#page-url').textContent === 'draft.test/next' && document.querySelector('#entry').value === ''`)); + const navigationDraft = await send({ type: 'get', args: { id: d.note.id } }); + const nextPage = await send({ type: 'current', args: { tabId: draftTab.id, capture: false } }); + check('pending edits stay on the original canonical page', navigationDraft.entries[0].text === 'Last draft before navigation' && nextPage.note === null, navigationDraft.entries[0].text); + + // A slow response for an earlier tab must not replace the latest view. + await panel.eval(`window.__holdCurrent = true;`); + await popup.eval(`chrome.tabs.update(${liTab.id}, { active: true })`); + check('earlier panel lookup can remain in flight', await panelWait(`typeof window.__releaseCurrent === 'function'`)); + await popup.eval(`chrome.tabs.update(${pr.id}, { active: true })`); + check('newer panel lookup renders while an earlier one is pending', await panelWait(`document.querySelector('#page-url').textContent === 'github.com/dataro/app/pull/482'`)); + await panel.eval(`window.__releaseCurrent();`); + check('late panel response does not replace the active page', await panel.eval(`document.querySelector('#page-url').textContent === 'github.com/dataro/app/pull/482' && document.querySelector('#entry').value === 'Reviewed. Waiting on CI.'`)); + check('automatic panel refreshes never request a capture', await panel.eval(`window.__panelCalls.filter((m) => m.type === 'current').every((m) => m.args.capture === false)`)); + + // linked pages in the sidebar: the current page is the GitHub PR, linked to the LinkedIn profile above + check('sidebar lists the linked page with its site', await panel.eval(`!document.querySelector('#linked').hidden && document.querySelectorAll('#linked-list li:not(.empty-row)').length === 1 && document.querySelector('#linked-list .host').textContent === 'linkedin.com'`), await panel.eval(`document.querySelector('#linked-list').textContent`)); + // the picker: type to find, Enter links; the linked page and this one are excluded + await popup.eval(`chrome.tabs.create({ url: 'https://example.com/plain-two', active: false })`); // a third page to pick (its note is created below) + await send({ type: 'addEntry', args: { url: 'https://example.com/plain-two', title: 'Plain Two', text: 'another page' } }); + await panel.eval(`document.querySelector('#add-link').click()`); + check('+ Link opens the type-to-find picker', await panelWait(`!document.querySelector('#link-picker').hidden && document.activeElement === document.querySelector('#link-picker input')`)); + check('picker excludes this page and already linked pages', await panelWait(`document.querySelectorAll('#link-picker .picker-list li').length > 0 && ![...document.querySelectorAll('#link-picker .picker-title')].some((t) => /LinkedIn|^github\\.com$/i.test(t.textContent))`), await panel.eval(`[...document.querySelectorAll('#link-picker .picker-title')].map((t) => t.textContent).join('|')`)); + await panel.eval(`const i = document.querySelector('#link-picker input'); i.value = 'plain two'; i.dispatchEvent(new Event('input'));`); + check('typing filters the pages', await panelWait(`document.querySelectorAll('#link-picker .picker-list li').length === 1 && document.querySelector('#link-picker .picker-title').textContent === 'Plain Two' && document.querySelector('#link-picker .picker-list li').classList.contains('active')`), await panel.eval(`document.querySelector('#link-picker .picker-status').textContent`)); + await screenshot(panel, 'sidebar-link-picker', 390, 860); + await panel.eval(`document.querySelector('#link-picker input').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))`); + check('Enter links the highlighted page', await panelWait(`document.querySelector('#link-picker').hidden && document.querySelectorAll('#linked-list li:not(.empty-row)').length === 2 && [...document.querySelectorAll('#linked-list .title')].some((t) => t.textContent === 'Plain Two')`), await panel.eval(`document.querySelector('#linked-list').textContent`)); + await panel.eval(`document.querySelector('#add-link').click()`); + await panel.eval(`document.querySelector('#link-picker input').dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`); + check('Esc closes the picker without closing the sidebar', await panelWait(`document.querySelector('#link-picker').hidden`) && await panel.eval(`!document.querySelector('main').hidden`)); + await panel.eval(`[...document.querySelectorAll('#linked-list li')].find((li) => li.querySelector('.title').textContent === 'Plain Two').querySelector('.x').click()`); + check('unlink from the list', await panelWait(`document.querySelectorAll('#linked-list li:not(.empty-row)').length === 1`)); + await panel.eval(`document.querySelector('#linked-list li:not(.empty-row)').click()`); + check('clicking a linked page shows it in the library at the page level', await panelWait(`!document.querySelector('#library').hidden && document.querySelector('#library-explorer').dataset.level === '2' && /LinkedIn/.test(document.querySelector('#library .explorer-preview h3')?.textContent || '')`), `level ${await panel.eval(`document.querySelector('#library-explorer').dataset.level`)} ${await panel.eval(`document.querySelector('#library .explorer-preview h3')?.textContent`)}`); + check('the library preview lists linked pages too', await panelWait(`document.querySelectorAll('#library .explorer-link').length === 1 && document.querySelector('#library .explorer-link .host').textContent === 'github.com'`)); + await panel.eval(`document.querySelector('#library .explorer-link').click()`); + check('a linked page in the preview navigates to that page', await panelWait(`document.querySelector('#library .explorer-preview h3')?.textContent === 'github.com' && document.querySelector('#library .explorer-site.selected')?.dataset.host === 'github.com'`)); + await screenshot(panel, 'sidebar-links', 390, 860); + await panel.eval(`document.querySelector('#return-to-page').click()`); + await panelWait(`document.querySelector('#library').hidden`); + // leave the notebook as the later counts expect it + const plainTwo = (await send({ type: 'list' })).find((r) => r.title === 'Plain Two'); + await send({ type: 'deleteNote', args: { id: plainTwo.id } }); + await popup.eval(`chrome.tabs.query({ url: 'https://example.com/plain-two' }).then((ts) => Promise.all(ts.map((t) => chrome.tabs.remove(t.id))))`); + + // share panel in the sidebar, on the page with stickies + await popup.eval(`chrome.tabs.move(${priceTab.id}, { windowId: ${liTab.windowId}, index: -1 }).then(() => chrome.tabs.update(${priceTab.id}, { active: true }))`); + check('sidebar offers Share on a page with stickies', await panelWait(`document.querySelector('#page-url').textContent.includes('/pricing') && !document.querySelector('#share-stickies').hidden`)); + await panel.eval(`document.querySelector('#share-stickies').click()`); + check('Share opens a panel with the encoded text ready to copy', await panelWait(`!document.querySelector('#share-panel').hidden && /^-----BEGIN MARGINALIA-----/.test(document.querySelector('#share-panel textarea').value) && /1 sticky/.test(document.querySelector('#share-panel .share-status').textContent)`), await panel.eval(`document.querySelector('#share-panel .share-status').textContent`)); + await panel.eval(`const p = document.querySelector('#share-panel input[type="password"]'); p.value = 'secret'; p.dispatchEvent(new Event('input'));`); + check('a passphrase switches the share to encrypted', await panelWait(`/aes-gcm/.test(document.querySelector('#share-panel textarea').value) && /encrypted/.test(document.querySelector('#share-panel .share-status').textContent)`)); + const encryptedShare = await panel.eval(`document.querySelector('#share-panel textarea').value`); + await screenshot(panel, 'sidebar-share', 390, 860); + await panel.eval(`document.querySelector('#import-stickies').click()`); + check('Import replaces the share panel with a paste box', await panelWait(`document.querySelector('#share-panel textarea')?.placeholder.startsWith('Paste') && document.querySelector('#share-stickies').getAttribute('aria-expanded') === 'false'`)); + await panel.eval(`document.querySelector('#share-panel textarea').value = ${JSON.stringify(encryptedShare)}; document.querySelector('#share-panel input[type="password"]').value = 'secret'; [...document.querySelectorAll('#share-panel button')].find((b) => b.textContent === 'Import').click();`); + check('importing my own share reports nothing new', await panelWait(`/Added 0 stickies/.test(document.querySelector('#share-panel .share-status').textContent)`), await panel.eval(`document.querySelector('#share-panel .share-status').textContent`)); + await panel.eval(`[...document.querySelectorAll('#share-panel button')].find((b) => b.textContent === 'Close').click()`); + check('the share panel closes', await panel.eval(`document.querySelector('#share-panel').hidden`)); + await popup.eval(`chrome.tabs.update(${pr.id}, { active: true })`); + await panelWait(`document.querySelector('#page-url').textContent === 'github.com/dataro/app/pull/482'`); + + // Browse saved notes inside the sidebar without changing the active tab. + // Back is hierarchical: page -> its site's pages -> all sites, never a toggle. + const level = () => panel.eval(`document.querySelector('#library-explorer').dataset.level`); + await panel.eval(`document.querySelector('#browse-notes').click()`); + check('sidebar back arrow lands on the current site\'s saved pages', await panelWait(`!document.querySelector('#library').hidden && document.querySelector('#library-explorer').dataset.level === '1' && document.querySelector('#library .explorer-site.selected')?.dataset.host === 'github.com' && document.querySelector('#library .explorer-page.selected')?.dataset.id === '${n2.id}'`), `level ${await level()}`); + check('page rows show a link count with the linked pages in the tooltip', await panel.eval(`(() => { const row = document.querySelector('#library .explorer-page[data-id="${n2.id}"]'); const c = row?.querySelector('.explorer-link-count'); return !!c && c.textContent.trim().endsWith('1') && /Linked to .*LinkedIn/.test(c.title) && !document.querySelector('#library .explorer-page[data-id="${liNote.id}"]')?.querySelector('.explorer-link-count')?.textContent.includes('2'); })()`), await panel.eval(`document.querySelector('#library .explorer-page[data-id="${n2.id}"] .explorer-link-count')?.title`)); + check('a site\'s page list is titled by the site, with no group headers', await panel.eval(`document.querySelector('#library-title').textContent === 'github.com' && document.querySelector('#library-sub').textContent === '1 saved page' && document.querySelectorAll('#library .explorer-group').length === 0 && document.querySelector('#library .explorer-pages > summary').textContent === 'github.com · 1'`), await panel.eval(`document.querySelector('#library-title').textContent + ' / ' + document.querySelector('#library-sub').textContent`)); + check('narrow library shows one level at a time', await panel.eval(`getComputedStyle(document.querySelector('#library .explorer-sites')).display === 'none' && getComputedStyle(document.querySelector('#library .explorer-pages')).display !== 'none' && getComputedStyle(document.querySelector('#library .explorer-preview')).display === 'none'`)); + check('library up arrow is offered below the top level and the return link replaces All notes', await panel.eval(`!document.querySelector('#library-up').hidden && document.querySelector('#open-all').hidden && !document.querySelector('#return-to-page').hidden`)); + await panel.eval(`document.querySelector('#library-up').click()`); + check('up from a site\'s pages reaches all sites and hides the arrow', await panelWait(`document.querySelector('#library-explorer').dataset.level === '0' && document.querySelector('#library-up').hidden && !document.querySelector('#library').hidden`), `level ${await level()}`); + await panel.eval(`document.querySelector('#library .explorer-site[data-host=""]').click()`); + check('all sites lists every saved page', await panelWait(`document.querySelector('#library-explorer').dataset.level === '1' && document.querySelectorAll('#library .explorer-page').length === 5`), `${await panel.eval(`document.querySelectorAll('#library .explorer-page').length`)} pages`); + check('across all sites the pages are listed by site', await panel.eval(`(() => { const groups = [...document.querySelectorAll('#library .explorer-group .explorer-label')].map((g) => g.textContent); const order = [...document.querySelectorAll('#library .explorer-list[aria-label="Saved pages"] > *')].map((n) => n.classList.contains('explorer-group') ? 'G:' + n.querySelector('.explorer-label').textContent : 'P:' + n.querySelector('.explorer-page-excerpt').textContent); return groups.length === 5 && groups.join() === [...groups].sort().join() && order[0].startsWith('G:') && order[1].startsWith('P:'); })()`), await panel.eval(`[...document.querySelectorAll('#library .explorer-group .explorer-label')].map((g) => g.textContent).join(',')`)); + check('library header names the level', await panel.eval(`document.querySelector('#library-title').textContent === 'All pages' && /5 saved pages, listed by site/.test(document.querySelector('#library-sub').textContent) && document.querySelector('#library .explorer-pages > summary').textContent === 'All pages · 5'`), await panel.eval(`document.querySelector('#library-title').textContent + ' / ' + document.querySelector('#library-sub').textContent`)); + await panel.eval(`document.querySelector('#library .explorer-page[data-id="${liNote.id}"]').click()`); + check('sidebar previews a different saved page inline', await panelWait(`document.querySelector('#library-explorer').dataset.level === '2' && document.querySelector('#library .explorer-preview').textContent.includes('Screening for the CSE role.')`) && (await popup.eval(`chrome.tabs.query({ active: true, windowId: ${liTab.windowId} })`))[0].id === pr.id); + check('breadcrumb names the ancestors and the page', await panel.eval(`[...document.querySelectorAll('#library .explorer-trail .crumb')].map((c) => c.textContent).join('|')`) === `All sites|All pages|${await panel.eval(`document.querySelector('#library .explorer-preview h3').textContent`)}`); + check('library header shows the page and its site at the page level', await panel.eval(`document.querySelector('#library-title').textContent === document.querySelector('#library .explorer-preview h3').textContent && document.querySelector('#library-sub').textContent === 'linkedin.com'`), await panel.eval(`document.querySelector('#library-title').textContent + ' / ' + document.querySelector('#library-sub').textContent`)); + await screenshot(panel, 'sidebar-explorer', 390, 860, true); + await panel.eval(`document.querySelector('#library-up').click()`); + check('up from a page returns to its list, not to the current page', await panelWait(`document.querySelector('#library-explorer').dataset.level === '1' && !document.querySelector('#library').hidden`), `level ${await level()}`); + await panel.eval(`document.querySelector('#library .explorer-trail .crumb').click()`); + check('breadcrumb jumps to the top', await panelWait(`document.querySelector('#library-explorer').dataset.level === '0'`)); + await panel.eval(`document.querySelector('#return-to-page').click()`); + check('sidebar returns to the current page by its own control', await panelWait(`document.querySelector('#library').hidden && !document.querySelector('main').hidden && document.querySelector('#page-url').textContent === 'github.com/dataro/app/pull/482' && !document.querySelector('#open-all').hidden`)); + check('sidebar has a close control', await panel.eval(`!!document.querySelector('#close-view') && getComputedStyle(document.querySelector('#close-view')).display !== 'none'`)); + await screenshot(panel, 'sidebar-current', 390, 860, true); + + // launch mode: popup, docked sidebar or the floating widget on the page + const launchState = async () => `${(await popup.eval(`chrome.action.getPopup({})`)).replace(/^chrome-extension:\/\/[^/]+\//, '')}|${await popup.eval(`chrome.sidePanel.getPanelBehavior().then((b) => b.openPanelOnActionClick)`)}`; + const setLaunch = async (settings) => { await popup.eval(`chrome.storage.sync.set({ settings: ${JSON.stringify(settings)} })`); await send({ type: 'settingsChanged' }); await sleep(300); }; + await setLaunch({ launch: 'sidebar' }); + check('sidebar mode clears the popup so the icon toggles the sidebar', await launchState() === '|true', await launchState()); + await setLaunch({ launch: 'floating' }); + check('floating mode clears the popup and the panel behaviour so the click reaches the worker', await launchState() === '|false', await launchState()); + await setLaunch({ openInSidePanel: true }); + check('the old sidebar switch still means the sidebar', await launchState() === '|true', await launchState()); + await setLaunch({ launch: 'popup' }); + check('popup mode restores the popup', await launchState() === 'src/popup.html|false', await launchState()); + const layout = await send({ type: 'widgetLayout', args: { layout: { fab: { x: 40, y: 50 } } } }); + const layout2 = await send({ type: 'widgetLayout', args: { layout: { widget: { x: 1, y: 2 } } } }); + check('widget layout is remembered and merged', layout.fab?.x === 40 && layout2.fab?.y === 50 && layout2.widget?.x === 1 && JSON.stringify(await send({ type: 'widgetLayout' })) === JSON.stringify(layout2), JSON.stringify(layout2)); + + // the panel page embedded in the widget binds to its tab and asks the layer to close it + await popup.eval(`chrome.tabs.create({ url: 'chrome-extension://${extId}/src/panel.html?embed=1', active: false })`); + const embedT = await waitFor((t) => t.url.endsWith('/src/panel.html?embed=1'), 'embedded panel'); + const embed = await CDP.connect(embedT.webSocketDebuggerUrl); + await sleep(800); + check('embedded panel marks itself and hides its own close control', await embed.eval(`document.body.classList.contains('embed') && document.body.classList.contains('panel') && getComputedStyle(document.querySelector('#close-view')).display === 'none'`)); + await embed.eval(`window.__closeMsg = null; window.addEventListener('message', (e) => { window.__closeMsg = e.data; }); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); true`); + let closeMsg = null; + for (let i = 0; i < 30; i++) { closeMsg = await embed.eval(`window.__closeMsg`); if (closeMsg) break; await sleep(100); } + check('Esc in the embedded panel asks the layer to close the widget', closeMsg?.type === 'pn:widget-close', JSON.stringify(closeMsg)); + embed.ws.close(); + await popup.eval(`chrome.tabs.query({ url: 'chrome-extension://${extId}/src/panel.html?embed=1' }).then((ts) => Promise.all(ts.map((t) => chrome.tabs.remove(t.id))))`); + + // options page lists notes + await browser.send('Target.createTarget', { url: `chrome-extension://${extId}/src/options.html` }); + const optT = await waitFor((t) => t.url.endsWith('/src/options.html'), 'options'); + const opt = await CDP.connect(optT.webSocketDebuggerUrl); + await sleep(1200); + const vaultState = await opt.eval(`typeof window.showDirectoryPicker === 'function' ? 'supported:' + document.querySelector('#vault-unsupported').hidden : 'unsupported:' + document.querySelector('#vault-unsupported').hidden + ':' + document.querySelector('#vault-connect').disabled`); + check('vault section explains itself when folder access is unavailable', vaultState === 'supported:true' || vaultState === 'unsupported:false:true', vaultState); + const optRows = await opt.eval(`document.querySelectorAll('#note-list .explorer-page').length`); + check('options page lists notes with entries or stickies', optRows === 5, `${optRows} rows`); + const optWait = async (expression) => { for (let i = 0; i < 50; i++) { if (await opt.eval(expression)) return true; await sleep(100); } return false; }; + await opt.eval(`document.querySelector('.explorer-site[data-host="linkedin.com"]').click()`); + check('explorer groups pages by site and previews notes', await optWait(`document.querySelectorAll('.explorer-page').length === 1 && document.querySelector('.explorer-preview').textContent.includes('Screening for the CSE role.')`)); + await opt.eval(`document.querySelector('.explorer-site[data-host=""]').click(); document.querySelector('.explorer-page[data-id="${priceRow.id}"]').click();`); + check('explorer previews sticky-only pages without calling them capture-only', await optWait(`document.querySelector('.explorer-preview').textContent.includes('Lead with the annual price, then monthly.')`) && await opt.eval(`document.querySelector('.explorer-page[data-id="${priceRow.id}"]').textContent.includes('1 sticky')`)); + await opt.send('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false }); + check('wide explorer expands both navigation columns after resizing', await optWait(`document.querySelector('.explorer-sites').open && document.querySelector('.explorer-pages').open`)); + await screenshot(opt, 'explorer-light', 1440, 900); + await screenshot(opt, 'explorer-dark', 1440, 900, true); + if (process.env.E2E_SCREENSHOTS) { await opt.eval(`document.querySelector('#settings').scrollIntoView(); true`); await screenshot(opt, 'settings-light', 1440, 900); await opt.eval(`window.scrollTo(0, 0); true`); } + await opt.eval(`document.querySelector('#search').value = 'annual price'; document.querySelector('#search').dispatchEvent(new Event('input'));`); + check('explorer search includes sticky text', await optWait(`document.querySelectorAll('.explorer-page').length === 1 && document.querySelector('.explorer-page').dataset.id === '${priceRow.id}'`)); + await opt.eval(`document.querySelector('#search').value = 'no matches for this phrase'; document.querySelector('#search').dispatchEvent(new Event('input'));`); + check('explorer has a useful empty search state', await optWait(`document.querySelectorAll('.explorer-page').length === 0 && document.querySelector('.explorer-preview').textContent.includes('No matching notes')`)); + await opt.eval(`document.querySelector('#search').value = ''; document.querySelector('#search').dispatchEvent(new Event('input')); document.querySelector('#include-captures').click();`); + check('explorer can include capture-only pages', await optWait(`document.querySelectorAll('.explorer-page').length === 6`)); + const opened = await send({ type: 'openNote', args: { id: priceRow.id, page: true } }); + const tabReaches = async (id, url) => { for (let i = 0; i < 40; i++) { if ((await popup.eval(`chrome.tabs.get(${id})`)).url === url) return true; await sleep(100); } return false; }; + check('openNote opens the selected saved page', await tabReaches(opened.tabId, priceTab.url)); + await popup.eval(`chrome.tabs.remove(${opened.tabId})`); + const openedDetail = await send({ type: 'openNote', args: { id: priceRow.id } }); + check('openNote can open full note details', await tabReaches(openedDetail.tabId, `chrome-extension://${extId}/src/note.html?id=${priceRow.id}`)); + const detailTarget = await waitFor((t) => t.url.endsWith(`/src/note.html?id=${priceRow.id}`), 'sticky details'); + const stickyDetail = await CDP.connect(detailTarget.webSocketDebuggerUrl); + let hasSticky = false; + for (let i = 0; i < 40; i++) { hasSticky = await stickyDetail.eval(`document.querySelector('#stickies')?.textContent.includes('Lead with the annual price, then monthly.')`); if (hasSticky) break; await sleep(100); } + check('full details include saved sticky comments', hasSticky); + stickyDetail.ws.close(); + await popup.eval(`chrome.tabs.remove(${openedDetail.tabId})`); +} catch (e) { + console.error('E2E ERROR', e); + failures++; +} finally { + chrome.kill('SIGKILL'); + await sleep(300); + rmSync(profile, { recursive: true, force: true }); + server.close(); + console.log(failures ? `\n${failures} FAILURE(S)` : '\nALL PASSED'); + process.exit(failures ? 1 : 0); +} diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs new file mode 100644 index 0000000..bbc3909 --- /dev/null +++ b/scripts/screenshots.mjs @@ -0,0 +1,133 @@ +// Renders README images and 1280x800 Chrome Web Store screenshots from the +// holodeck: seeds notes (with a fixture profile capture), captures the popup, +// the note detail page and the options page, and composes each onto a stage. +// +// make screenshots +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const EXT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const NAME = JSON.parse(readFileSync(join(EXT, 'manifest.json'), 'utf8')).name; +const OUT = join(EXT, 'promo'); +const STORE = join(OUT, 'store'); +const CANDIDATES = [process.env.BROWSER, '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/usr/bin/chromium', '/usr/bin/chromium-browser'].filter(Boolean); +const CHROME = CANDIDATES.find((c) => existsSync(c)); +if (!CHROME) { console.error('No Chromium-based browser found. Set BROWSER=/path/to/binary'); process.exit(2); } +const PORT = Number(process.env.PORT) || 9339; +const HOLODECK = process.env.HOLODECK || join(homedir(), '.holodeck'); +mkdirSync(join(HOLODECK, 'tmp'), { recursive: true }); +mkdirSync(STORE, { recursive: true }); +const profile = mkdtempSync(join(HOLODECK, 'tmp', 'notes-shots-')); +const chrome = spawn(CHROME, [`--user-data-dir=${profile}`, `--load-extension=${EXT}`, `--remote-debugging-port=${PORT}`, '--headless=new', '--host-resolver-rules=MAP * ~NOTFOUND', '--no-first-run', '--no-default-browser-check', ...(process.env.CI ? ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : []), 'about:blank'], { stdio: 'ignore' }); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const targets = async () => (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); +async function waitFor(pred, tries = 60) { for (let i = 0; i < tries; i++) { try { const t = (await targets()).find(pred); if (t) return t; } catch (e) { /* not up */ } await sleep(250); } throw new Error('timeout waiting for target'); } +class CDP { + constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); ws.addEventListener('message', (m) => { const msg = JSON.parse(m.data); if (msg.id && this.pending.has(msg.id)) { const { res, rej } = this.pending.get(msg.id); this.pending.delete(msg.id); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); } }); } + static async connect(url) { const ws = new WebSocket(url); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; }); return new CDP(ws); } + send(m, p = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method: m, params: p })); return new Promise((res, rej) => this.pending.set(id, { res, rej })); } + async eval(e) { const r = await this.send('Runtime.evaluate', { expression: e, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails)); return r.result.value; } + async shot(file, { width, height, scale = 2, dark = false }) { + await this.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: scale, mobile: false }); + await this.send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: dark ? 'dark' : 'light' }] }); + await sleep(350); + const r = await this.send('Page.captureScreenshot', { format: 'png' }); + writeFileSync(file, Buffer.from(r.data, 'base64')); + return file; + } +} +async function findExtensionId() { + for (let i = 0; i < 60; i++) { + let list = []; + try { list = await targets(); } catch (e) { /* not up */ } + for (const t of list.filter((t) => t.type === 'service_worker' && t.url.endsWith('/src/background.js'))) { + try { const cdp = await CDP.connect(t.webSocketDebuggerUrl); const name = await cdp.eval('chrome.runtime.getManifest().name'); cdp.ws.close(); if (name === NAME) return new URL(t.url).host; } catch (e) { /* next */ } + } + await sleep(250); + } + throw new Error('extension not found'); +} +function stageHtml({ png, title, caption, wide = false }) { + const data = readFileSync(png).toString('base64'); + const icon = readFileSync(join(EXT, 'img', 'icon32.png')).toString('base64'); + return `

${title}

${caption}

${NAME}
`; +} +try { + const extId = await findExtensionId(); + const info = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json(); + const browser = await CDP.connect(info.webSocketDebuggerUrl); + await browser.send('Target.createTarget', { url: `chrome-extension://${extId}/src/popup.html`, newWindow: true, width: 360, height: 640 }); + const popupT = await waitFor((t) => t.url.endsWith('/src/popup.html')); + const popup = await CDP.connect(popupT.webSocketDebuggerUrl); + for (let i = 0; i < 40; i++) { if (await popup.eval(`typeof chrome !== 'undefined' && !!chrome.windows && location.protocol === 'chrome-extension:'`).catch(() => false)) break; await sleep(250); } + const send = (type, args) => popup.eval(`new Promise((res, rej) => chrome.runtime.sendMessage({ type: ${JSON.stringify(type)}, args: ${JSON.stringify(args)} }, (r) => r && r.ok ? res(r.result) : rej(new Error(r ? r.error : 'x'))))`); + const w = await popup.eval(`chrome.windows.create({ url: ['https://www.linkedin.com/in/janedoe/', 'https://github.com/dataro/app/pull/482/files', 'https://dataro.atlassian.net/browse/ENG-1201'], focused: true })`); + await sleep(2000); + const tabs = await popup.eval('chrome.tabs.query({})'); + const u = (t) => t.url || t.pendingUrl || ''; + const li = tabs.find((t) => u(t).includes('linkedin')); const pr = tabs.find((t) => u(t).includes('github')); const jira = tabs.find((t) => u(t).includes('atlassian')); + if (!li || !pr || !jira) throw new Error(`seed tabs missing: ${tabs.map(u).join(' | ')}`); + const fixture = readFileSync(join(EXT, 'test', 'fixtures', 'linkedin-overlap.txt'), 'utf8'); + await send('addEntry', { tabId: li.id, title: 'Jane Doe | LinkedIn', text: 'Likes cheese. Two kids, dog named Fluffy.', snapshot: { title: 'Jane Doe | LinkedIn', text: fixture, meta: { siteName: 'LinkedIn' } } }); + await send('addEntry', { tabId: li.id, text: '> "Head of Things at Acme since 2024"\nMet at the Sydney meetup. Follow up re: the Q4 budget.', snapshot: { title: 'Jane Doe | LinkedIn', text: fixture.replace('Jan 2021 - Feb 2025 · 4 yrs 2 mos', 'Jan 2021 - Feb 2023 · 2 yrs 2 mos') } }); + await send('addEntry', { tabId: pr.id, title: 'Fix consolidate target · Pull Request #482', text: 'Reviewed. Waiting on CI.' }); + await send('addEntry', { tabId: jira.id, title: '[ENG-1201] Dynamics sync retries', text: 'Blocked on the vendor. Check again Monday.' }); + const list = await send('list', {}); + const liRow = list.find((r) => r.host === 'linkedin.com'); + if (!liRow) throw new Error(`linkedin note missing: ${JSON.stringify(list.map((r) => r.host))}`); + const liId = liRow.id; + await send('setReminder', { id: list.find((r) => r.host.includes('atlassian')).id, preset: 'nextweek' }); + await popup.eval(`chrome.tabs.update(${li.id}, { active: true })`); await popup.eval(`chrome.windows.update(${w.id}, { focused: true })`); + await popup.eval('location.reload()'); await sleep(1200); + const shots = {}; + shots.popup = await popup.shot(join(OUT, 'popup.png'), { width: 360, height: 640 }); + await popup.shot(join(OUT, 'popup-dark.png'), { width: 360, height: 640, dark: true }); + await popup.eval(`chrome.tabs.update(${jira.id}, { active: true })`); + await popup.eval('location.reload()'); await sleep(1200); + await popup.eval(`const t = document.querySelector('#entry'); t.value = 'Blocked on the vendor. Check again Monday.'; t.dispatchEvent(new Event('input'));`); await sleep(900); + shots.remind = await popup.shot(join(OUT, 'popup-remind.png'), { width: 360, height: 640 }); + await popup.eval(`chrome.tabs.update(${li.id}, { active: true })`); + await popup.eval('location.reload()'); await sleep(1200); + await browser.send('Target.createTarget', { url: `chrome-extension://${extId}/src/note.html?id=${liId}`, newWindow: true, width: 900, height: 760 }); + const noteT = await waitFor((t) => t.url.includes('/src/note.html')); + const note = await CDP.connect(noteT.webSocketDebuggerUrl); + await sleep(1200); + shots.detail = await note.shot(join(OUT, 'note-detail.png'), { width: 900, height: 760, scale: 1 }); + await browser.send('Target.createTarget', { url: `chrome-extension://${extId}/src/options.html`, newWindow: true, width: 1100, height: 760 }); + const optT = await waitFor((t) => t.url.endsWith('/src/options.html')); + const opt = await CDP.connect(optT.webSocketDebuggerUrl); + await sleep(1200); + shots.options = await opt.shot(join(OUT, 'options.png'), { width: 1100, height: 760, scale: 1 }); + const stages = [ + { file: '1-popup.png', png: shots.popup, title: 'A note that follows the page', caption: 'Write on any page. The note is there next time, even when the URL is different: a pull request and its Files tab, a profile and its subpages. Dated entries, a count on the icon.' }, + { file: '2-insights.png', png: shots.detail, title: 'A local record of what you saw', caption: 'Every save keeps what the page looked like. When it changes, you see what changed. On a profile, you see what a careful reader would ask about.', wide: true }, + { file: '3-remind.png', png: shots.remind, title: 'Remind me. Snooze that tab.', caption: 'Come back in an hour, tonight, tomorrow or next week. Snooze a tab: close it now, get it back later.' }, + { file: '4-notes.png', png: shots.options, title: 'Your notes are files', caption: 'Markdown with YAML frontmatter that Obsidian reads natively. Export everything as a zip, or connect a vault folder and every save lands there.', wide: true } + ]; + for (const st of stages) { + const html = join(profile, `${st.file}.html`); + writeFileSync(html, stageHtml(st)); + const { targetId } = await browser.send('Target.createTarget', { url: `file://${html}`, newWindow: true, width: 1280, height: 800 }); + const t = await waitFor((x) => x.id === targetId || x.url === `file://${html}`); + const page = await CDP.connect(t.webSocketDebuggerUrl); + await sleep(500); + await page.shot(join(STORE, st.file), { width: 1280, height: 800, scale: 1, dark: true }); + page.ws.close(); + console.log('wrote', join('promo/store', st.file)); + } + console.log('wrote', Object.values(shots).map((f) => f.replace(EXT + '/', '')).join(', ')); +} finally { chrome.kill('SIGKILL'); await sleep(300); rmSync(profile, { recursive: true, force: true }); } diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..8bf37f2 --- /dev/null +++ b/src/background.js @@ -0,0 +1,823 @@ +// Service worker: owns every operation. The popup and options page only send +// messages. Presence badge, reminders, snoozed tabs, context menu capture. + +import { loadSettings } from './lib/settings.js'; +import { compileRules } from './lib/url.js'; +import { + addEntry, addSnapshot, addSticky, feedbackMarkdown, fileNameFor, fromMarkdown, identify, linkNotes, mergeNotes, matchesQuery, newNote, noteId, + presetTime, removeEntry, removeSticky, setReminder, setToday, toMarkdown, todayIndex, unlinkNotes, updateEntry, updateSticky +} from './lib/notes.js'; + +// The popup does not need snapshot bodies; keep its messages small. +function stripForPopup(note) { + return { ...note, snapshots: (note.snapshots || []).map(({ text, ...rest }) => ({ ...rest, chars: text?.length || 0 })), snapshot: note.snapshot ? { at: note.snapshot.at, title: note.snapshot.title } : null }; +} +import { allNotes, deleteNote, getIndex, getNote, migrateNoteTitles, putNote, rebuildIndex, vaultFolder } from './lib/store.js'; +import { STATE_KEY, STATUS_KEY, SYNC_ALARM, SYNC_PERIOD_MINUTES, canObserve, changeCollector, emptyState, hashText, memoryFolder, observeHandle, syncFolder, worthAFile } from './lib/vault.js'; +import { loadVaultHandle } from './lib/store.js'; +import { applyShare, decodeShare, encodeShare, shareFileName, sharePayload } from './lib/share.js'; +import { makeZip } from './lib/zip.js'; +import { analyzeProfile, captureBudget, diffLines, diffProfiles, extractorFor } from './lib/profile.js'; + +const storageReady = migrateNoteTitles(); +const VISIBILITY_KEY = 'stickies:visible'; +const WIDGET_KEY = 'widget:layout'; +async function stickyVisibility() { + return (await chrome.storage.local.get(VISIBILITY_KEY))[VISIBILITY_KEY] !== false; +} + +// ----- context -------------------- + +async function ctx() { + const { settings } = await loadSettings(); + const { compiled } = compileRules(settings.rules); + return { settings, compiled }; +} + +async function noteForUrl(url) { + const c = await ctx(); + const ident = identify(url, c.compiled, c.settings.normalization); + const note = await getNote(ident.id); + return { ident, note, settings: c.settings }; +} + +async function tabFor(args) { + if (args.tabId) { + try { return await chrome.tabs.get(args.tabId); } catch (e) { /* gone */ } + } + // a side panel belongs to one window: follow that window's active tab + const [t] = await chrome.tabs.query(args.windowId ? { active: true, windowId: args.windowId } : { active: true, lastFocusedWindow: true }); + return t || null; +} + +// Page capture via the activeTab grant from the user's click: title, core +// metadata (canonical, OpenGraph, author, published, JSON-LD types) and the +// page text, line structure preserved so site parsers can read it. +async function capturePage(tabId, { maxChars = 6000 } = {}) { + try { + const [r] = await chrome.scripting.executeScript({ + target: { tabId }, + args: [maxChars], + func: (limit) => { + const q = (sel) => document.querySelector(sel)?.getAttribute('content') || ''; + const meta = { + canonical: document.querySelector('link[rel="canonical"]')?.href || '', + siteName: q('meta[property="og:site_name"]'), + ogType: q('meta[property="og:type"]'), + ogTitle: q('meta[property="og:title"]'), + author: q('meta[name="author"]') || q('meta[property="article:author"]'), + published: q('meta[property="article:published_time"]') || q('meta[name="date"]'), + modified: q('meta[property="article:modified_time"]'), + lang: document.documentElement.lang || '', + jsonLdTypes: [...document.querySelectorAll('script[type="application/ld+json"]')].flatMap((s) => { + try { const j = JSON.parse(s.textContent); return [].concat(j).map((x) => x && x['@type']).filter(Boolean); } catch (e) { return []; } + }).slice(0, 8) + }; + const text = (document.body?.innerText || '').replace(/[ \t]+/g, ' ').replace(/\n{2,}/g, '\n').trim(); + return { + title: document.title, + description: q('meta[name="description"]') || q('meta[property="og:description"]'), + selection: String(window.getSelection() || '').slice(0, 2000), + text: text.slice(0, limit), + truncated: text.length > limit, + meta + }; + } + }); + return r?.result || null; + } catch (e) { + return null; // no activeTab grant (no user gesture), chrome:// page, etc. + } +} + +// Runs the site extractor for the page's rule, if any, on a capture. +function structure(ident, capture) { + const extractor = extractorFor(ident?.rule); + if (!extractor || !capture?.text) return null; + try { return extractor(capture.text); } catch (e) { return null; } +} + +// One place for "capture this tab for this page identity": the budget comes +// from the parser registry, and the structured parse rides along. Tests pass +// a ready capture in args.snapshot. +async function captureFor(tab, ident, args) { + const cap = args.snapshot || (tab ? await capturePage(tab.id, { maxChars: captureBudget(ident.rule) }) : null); + return cap ? { ...cap, structured: structure(ident, cap) } : null; +} + +// Insights for a note: analysis of the newest structured capture, edits since +// the previous capture, and a plain-text diff for any page. +function insightsFor(note) { + const snaps = note?.snapshots || []; + if (snaps.length === 0) return null; + const newest = snaps[snaps.length - 1]; + const previous = snaps.length > 1 ? snaps[snaps.length - 2] : null; + const out = { capturedAt: newest.at, seenAt: newest.seen, captures: snaps.length, meta: newest.meta || null, kind: newest.structured?.kind || null }; + if (newest.structured) { + const { findings, summary } = analyzeProfile(newest.structured); + out.findings = findings; + out.summary = summary; + out.profile = { name: newest.structured.name, headline: newest.structured.headline, location: newest.structured.location, experience: newest.structured.experience, education: newest.structured.education }; + if (previous?.structured) out.edits = diffProfiles(previous.structured, newest.structured); + } + if (previous) { + const d = diffLines(previous.text, newest.text); + out.changed = { since: previous.at, titleChanged: previous.title !== newest.title, added: d.added.slice(0, 12), removed: d.removed.slice(0, 12), addedCount: d.added.length, removedCount: d.removed.length }; + } + return out; +} + +async function persist(note, settings) { + const saved = await putNote(note); + if (saved && settings.writeToVault) await vaultWrite(saved); + await syncReminder(saved || note); + scheduleBadgeForAll(); + return saved; +} + +// ----- vault: the folder shared between profiles -------------------- + +async function vaultState() { return (await chrome.storage.local.get(STATE_KEY))[STATE_KEY] || emptyState(); } +async function setVaultState(state) { await chrome.storage.local.set({ [STATE_KEY]: state }); } +async function vaultStatus() { return (await chrome.storage.local.get(STATUS_KEY))[STATUS_KEY] || { result: 'never' }; } +async function setVaultStatus(patch) { await chrome.storage.local.set({ [STATUS_KEY]: { ...(await vaultStatus()), ...patch } }); } + +// One note, written now (a save). The sync records what the folder holds so +// the next scan does not read our own write back. +async function vaultWrite(note) { + const { status, folder } = await vaultFolder(); + if (status !== 'ok') { await setVaultStatus({ result: status }); return status; } + if (!worthAFile(note)) return 'skipped'; + try { + const state = await vaultState(); + const text = toMarkdown(note); + const name = fileNameFor(note); + const meta = await folder.write(name, text); + for (const [n, r] of Object.entries(state.files)) if (r.id === note.id && n !== name) { await folder.remove(n).catch(() => {}); delete state.files[n]; } + state.files[name] = { id: note.id, mtime: meta.mtime, size: meta.size, hash: hashText(text) }; + await setVaultState(state); + return 'written'; + } catch (e) { console.warn('vault write failed', e); await setVaultStatus({ result: 'error', error: e.message }); return 'error'; } +} + +async function vaultRemove(id) { + const state = await vaultState(); + const entry = Object.entries(state.files).find(([, r]) => r.id === id); + if (!entry) return; + const { status, folder } = await vaultFolder(); + if (status === 'ok') await folder.remove(entry[0]).catch(() => {}); + // with no access the record stays, and the next sync removes the file + if (status === 'ok') { delete state.files[entry[0]]; await setVaultState(state); } +} + +// The two-way reconciliation, serialised so an alarm and a click cannot overlap. +// Tests pass an in-memory folder in args.folder and get it back. +let syncing = null; +function syncVault(args = {}) { + if (syncing) return syncing; + syncing = (async () => { + const seam = args.folder ? memoryFolder(args.folder.files || {}) : null; + let folder = seam, status = 'ok'; + if (!seam) ({ status, folder } = await vaultFolder()); + if (status !== 'ok') { await setVaultStatus({ result: status, at: new Date().toISOString() }); return { status, report: null }; } + const store = { allNotes, putNote, deleteNote: async (id) => { await deleteNote(id); await chrome.alarms.clear(`remind:${id}`); } }; + try { + const only = Array.isArray(args.only) && args.only.length ? new Set(args.only) : null; + const { state, report } = await syncFolder(folder, store, seam ? (args.state || emptyState()) : await vaultState(), { only }); + if (!seam) await setVaultState(state); + await setVaultStatus({ result: 'ok', at: new Date().toISOString(), report, error: null }); + if (report.imported || report.updated || report.deletedLocal) { await rearmReminders(); scheduleBadgeForAll(); } + if (!seam) watchVault(); + return { status: 'ok', report, ...(seam ? { folder: seam.files, state } : {}) }; + } catch (e) { + await setVaultStatus({ result: 'error', at: new Date().toISOString(), error: e.message }); + return { status: 'error', error: e.message, report: null }; + } + })().finally(() => { syncing = null; }); + return syncing; +} + +// A look at the current page is a good moment to catch up, at most once a minute. +async function maybeSyncVault() { + try { + const { settings } = await loadSettings(); + if (!settings.writeToVault) return; + const st = await vaultStatus(); + if (st.at && Date.now() - new Date(st.at).getTime() < 60000) return; + syncVault({ reason: 'view' }).catch(() => {}); + } catch (e) { /* ignore */ } +} + +// Event-driven: watch the folder while this worker lives. Chrome does not yet +// expose FileSystemObserver to service workers, so today this is a no-op and +// the open pages (side panel, widget, Settings) do the watching; if a browser +// adds it, the worker picks it up on every wake. The alarm and the +// look-at-a-page sync remain the safety net either way. +let vaultObserver = null, watchingHandle = null; +const vaultChanges = changeCollector((names) => syncVault({ reason: 'watch', only: names ? [...names] : undefined }).catch(() => {})); +async function watchVault() { + if (!canObserve()) return false; + try { + const handle = await loadVaultHandle(); + if (!handle) { stopWatchingVault(); return false; } + if (vaultObserver && watchingHandle && await handle.isSameEntry(watchingHandle)) return true; + if ((await handle.queryPermission({ mode: 'readwrite' })) !== 'granted') return false; + stopWatchingVault(); + vaultObserver = await observeHandle(handle, (change) => vaultChanges.add(change)); + watchingHandle = handle; + return !!vaultObserver; + } catch (e) { console.warn('vault watch failed', e); return false; } +} +function stopWatchingVault() { + try { vaultObserver?.disconnect(); } catch (e) { /* ignore */ } + vaultObserver = null; watchingHandle = null; +} + +async function scheduleVaultSync() { + try { await chrome.alarms.create(SYNC_ALARM, { periodInMinutes: SYNC_PERIOD_MINUTES }); } catch (e) { /* ignore */ } +} + +// ----- links -------------------- + +// The index rows for a note's links, by key. A link whose page was deleted or +// never imported is kept (the key still says what it was) and marked missing. +async function linkedRows(note) { + const keys = note?.links || []; + if (!keys.length) return []; + const index = await getIndex(); + return keys.map((key) => index[noteId(key)] || { id: noteId(key), key, title: key, host: '', url: '', missing: true }); +} + +// The note named by args.id, or the (possibly new) note for args.url / the +// tab: linking a page the user has not written on yet creates its note. +async function noteFor(args) { + if (args.id) { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + return note; + } + const tab = await tabFor(args); + const url = args.url || tab?.url; + if (!url || !/^(https?|file):/.test(url)) throw new Error('No page to link'); + const { ident, note } = await noteForUrl(url); + return note || newNote({ url, title: args.title || tab?.title || url, key: ident.key, rule: ident.rule }); +} + +// ----- messages -------------------- + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + handle(message) + .then((result) => sendResponse({ ok: true, result })) + .catch((error) => { console.error(message, error); sendResponse({ ok: false, error: error.message || String(error) }); }); + return true; +}); + +async function handle({ type, args = {} } = {}) { + await storageReady; + switch (type) { + case 'openNote': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + const url = args.page ? note.url : chrome.runtime.getURL(`src/note.html?id=${note.id}`); + if (args.page && !/^(https?|file):/i.test(url)) throw new Error('This page cannot be opened.'); + const tab = await chrome.tabs.create({ url }); + return { tabId: tab.id }; + } + case 'stickyVisibility': { + if (typeof args.visible === 'boolean') { + await chrome.storage.local.set({ [VISIBILITY_KEY]: args.visible }); + await Promise.all((await chrome.tabs.query({})).map((t) => chrome.tabs.sendMessage(t.id, { type: 'pn:visibility', visible: args.visible }).catch(() => {}))); + } + return { visible: await stickyVisibility() }; + } + case 'current': { + const tab = await tabFor(args); + if (args.capture !== false) maybeSyncVault(); + if (!tab || !tab.url) return { tab: null, note: null, vault: await vaultStatus() }; + let { ident, note, settings } = await noteForUrl(tab.url); + // Opening the popup is a user action on this tab: keep what the + // page looks like now. A page with no note yet gets a capture-only + // note, which stays out of the way until something is written. + if (settings.snapshotOnSave && args.capture !== false && /^https?:/.test(tab.url)) { + const cap = await captureFor(tab, ident, args); + if (cap) { + const base = note || newNote({ url: tab.url, title: tab.title || tab.url, key: ident.key, rule: ident.rule }); + const next = addSnapshot(base, cap); + if (next !== note) note = await putNote(next); + } + } + return { tab: { id: tab.id, url: tab.url, title: tab.title, favIconUrl: tab.favIconUrl || '' }, ident, note: note ? stripForPopup(note) : null, insights: note ? insightsFor(note) : null, today: note ? todayIndex(note) : -1, linked: await linkedRows(note), vault: await vaultStatus(), stickiesVisible: await stickyVisibility(), alwaysShow: /^https?:/.test(tab.url) ? await alwaysShowState(tab.url) : null }; + } + case 'capture': { + // user asked to keep what this page looks like, with no entry yet + const tab = await tabFor(args); + if (!tab?.url) throw new Error('No page to capture'); + const { ident, note, settings } = await noteForUrl(tab.url); + const cap = await captureFor(tab, ident, args); + if (!cap) throw new Error('Could not read this page. Chrome pages and some sites cannot be captured.'); + let next = note || newNote({ url: tab.url, title: tab.title || tab.url, key: ident.key, rule: ident.rule }); + next = addSnapshot(next, cap); + const saved = await persist(next, settings); + return { note: saved ? stripForPopup(saved) : null, insights: saved ? insightsFor(saved) : null }; + } + // ----- stickies: notes attached to elements on the page ----- + case 'stickies': { + const { note } = await noteForUrl(args.url); + return note?.stickies || []; + } + case 'stickyAdd': { + const { ident, note, settings } = await noteForUrl(args.url); + const base = note || newNote({ url: args.url, title: args.title || args.url, key: ident.key, rule: ident.rule }); + const next = addSticky(base, { anchor: args.anchor, text: args.text || '' }); + const saved = await persist(next, settings); + notifyLayers(args.url); + return saved.stickies[saved.stickies.length - 1]; + } + case 'stickyUpdate': { + const { note, settings } = await noteForUrl(args.url); + if (!note) throw new Error('Note not found'); + const next = updateSticky(note, args.id, { text: args.text }); + if (next !== note) await persist(next, settings); + return true; + } + case 'stickyRemove': { + const { note, settings } = await noteForUrl(args.url); + if (!note) throw new Error('Note not found'); + const next = removeSticky(note, args.id); + if (next !== note) await persist(next, settings); + notifyLayers(args.url); + return true; + } + case 'stickyPlacement': { + // the layer reports which stickies it could place, so lists can show orphans + const { note } = await noteForUrl(args.url); + if (!note) return true; + let next = note; + for (const p of args.placement || []) next = updateSticky(next, p.id, { found: p.found }); + if (next !== note) await putNote(next); + return true; + } + case 'feedback': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + return feedbackMarkdown(note); + } + case 'annotate': { + // inject the layer into the tab (activeTab grant from the user's click) + // and optionally start pick mode + const tab = await tabFor(args); + if (!tab?.id || !/^https?:/.test(tab.url || '')) throw new Error('Stickies work on web pages only.'); + await ensureLayer(tab.id); + if (args.pick) await chrome.tabs.sendMessage(tab.id, { type: 'pn:pick' }); + if (args.scrollTo) await chrome.tabs.sendMessage(tab.id, { type: 'pn:scrollTo', id: args.scrollTo }); + return true; + } + case 'alwaysShow': { + // per-site opt-in: register the layer to load on that origin + const origin = new URL(args.url).origin; + if (args.enable) { + const granted = await chrome.permissions.contains({ origins: [`${origin}/*`] }); + if (!granted) throw new Error('Permission was not granted.'); + await chrome.scripting.registerContentScripts([{ id: `layer:${origin}`, matches: [`${origin}/*`], js: ['src/content/annotate.js'], runAt: 'document_idle' }]).catch(async () => { + await chrome.scripting.updateContentScripts([{ id: `layer:${origin}`, matches: [`${origin}/*`], js: ['src/content/annotate.js'], runAt: 'document_idle' }]); + }); + } else { + await chrome.scripting.unregisterContentScripts({ ids: [`layer:${origin}`] }).catch(() => {}); + await chrome.permissions.remove({ origins: [`${origin}/*`] }).catch(() => {}); + } + return alwaysShowState(args.url); + } + case 'alwaysShowState': + return alwaysShowState(args.url); + case 'insights': { + const note = await getNote(args.id); + return note ? insightsFor(note) : null; + } + case 'detail': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + return { note, insights: insightsFor(note) }; + } + case 'addEntry': { + const tab = await tabFor(args); + const url = args.url || tab?.url; + if (!url) throw new Error('No page to attach the note to'); + const { ident, note, settings } = await noteForUrl(url); + let next = note || newNote({ url, title: args.title || tab?.title || url, key: ident.key, rule: ident.rule }); + next = addEntry(next, args.text); + if (settings.snapshotOnSave && tab) { + const cap = await captureFor(tab, ident, args); + if (cap) next = addSnapshot(next, cap); + } + const saved = await persist(next, settings); + return saved ? stripForPopup(saved) : saved; + } + case 'draft': { + // autosave from the popup editor: today's entry follows the text + const tab = await tabFor(args); + const url = args.url || tab?.url; + if (!url) throw new Error('No page to attach the note to'); + const { ident, note, settings } = await noteForUrl(url); + const base = note || newNote({ url, title: args.title || tab?.title || url, key: ident.key, rule: ident.rule }); + const next = setToday(base, args.text); + if (next === base && note) return { note: stripForPopup(note), today: todayIndex(note) }; + const saved = await persist(next, settings); + return { note: saved ? stripForPopup(saved) : null, today: saved ? todayIndex(saved) : -1 }; + } + case 'updateEntry': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + return persist(updateEntry(note, args.index, args.text), (await ctx()).settings); + } + case 'removeEntry': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + return persist(removeEntry(note, args.index), (await ctx()).settings); + } + case 'setReminder': { + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + const when = args.preset ? presetTime(args.preset) : args.when; + return persist(setReminder(note, when), (await ctx()).settings); + } + case 'snoozeTab': { + const tab = await tabFor(args); + if (!tab?.url) throw new Error('No tab to snooze'); + const when = args.preset ? presetTime(args.preset) : args.when; + if (!when) throw new Error('No time given'); + const { ident, note, settings } = await noteForUrl(tab.url); + let next = note || newNote({ url: tab.url, title: tab.title, key: ident.key, rule: ident.rule }); + next = addEntry(next, args.text || `Snoozed until ${new Date(when).toLocaleString()}.`); + next = setReminder(next, when); + await persist(next, settings); + await chrome.tabs.remove(tab.id); + return { when }; + } + case 'link': { + // args: { id | url | tabId } and { otherId }; symmetric, both notes saved + const [a, other] = await Promise.all([noteFor(args), getNote(args.otherId)]); + if (!other) throw new Error('That page has no note to link to'); + const { settings } = await ctx(); + const [a2, b2] = linkNotes(a, other); + const saved = (await persist(a2, settings)) || a2; + if (b2 !== other) await persist(b2, settings); + return { note: stripForPopup(saved), linked: await linkedRows(saved) }; + } + case 'unlink': { + const a = await getNote(args.id); + if (!a) throw new Error('Note not found'); + const other = await getNote(args.otherId); + const { settings } = await ctx(); + // the other side may be gone: drop the dangling key by itself + const [a2, b2] = other ? unlinkNotes(a, other) : [{ ...a, links: (a.links || []).filter((k) => noteId(k) !== args.otherId) }, null]; + const saved = (await persist(a2, settings)) || a2; + if (b2 && b2 !== other) await persist(b2, settings); + return { note: stripForPopup(saved), linked: await linkedRows(saved) }; + } + case 'links': + return linkedRows(await getNote(args.id)); + case 'deleteNote': { + // the pages it was linked to forget it + const gone = await getNote(args.id); + if (gone?.links?.length) { + const { settings } = await ctx(); + for (const key of gone.links) { + const other = await getNote(noteId(key)); + if (other) await persist(unlinkNotes(other, gone)[0], settings); + } + } + await deleteNote(args.id); + await vaultRemove(args.id); + await chrome.alarms.clear(`remind:${args.id}`); + scheduleBadgeForAll(); + return true; + } + case 'get': + return getNote(args.id); + case 'list': { + let index = await getIndex(); + // rows saved before links were indexed by key: rebuild once + if (Object.values(index).some((r) => r.links > 0 && !Array.isArray(r.linkKeys))) { await rebuildIndex(); index = await getIndex(); } + let rows = Object.values(index); + if (!args.includeCaptures) rows = rows.filter((r) => r.entries > 0 || r.remind || r.stickies > 0); + if (args.query) { + const notes = await allNotes(); + const ids = new Set(notes.filter((n) => matchesQuery(n, args.query)).map((n) => n.id)); + rows = rows.filter((r) => ids.has(r.id)); + } + rows.sort((a, b) => b.updated.localeCompare(a.updated)); + // the pages each row is linked to, resolved against the whole index + return rows.map((r) => ({ ...r, linked: (r.linkKeys || []).map((key) => { const o = index[noteId(key)]; return o ? { id: o.id, title: o.title, host: o.host } : { id: noteId(key), title: key, host: '', missing: true }; }) })); + } + case 'reminders': { + const index = await getIndex(); + return Object.values(index).filter((r) => r.remind).sort((a, b) => a.remind.localeCompare(b.remind)); + } + case 'export': { + const notes = await allNotes(); + const files = notes.map((n) => ({ name: fileNameFor(n), text: toMarkdown(n) })); + // captures ride along as plain text so the local cache is backed up too + for (const n of notes) { + for (const c of n.snapshots || []) { + const stamp = c.at.replace(/[:.]/g, '-'); + const head = [`title: ${c.title || ''}`, `url: ${n.url}`, `captured: ${c.at}`, c.seen && c.seen !== c.at ? `seen: ${c.seen}` : null, c.meta?.siteName ? `site: ${c.meta.siteName}` : null, c.meta?.author ? `author: ${c.meta.author}` : null, c.meta?.published ? `published: ${c.meta.published}` : null].filter(Boolean).join('\n'); + files.push({ name: `captures/${fileNameFor(n).replace(/\.md$/, '')}/${stamp}.txt`, text: `${head}\n\n${c.text || ''}` }); + } + } + const zip = makeZip(files); + // base64 so it crosses the message boundary + let bin = ''; + for (let i = 0; i < zip.length; i += 0x8000) bin += String.fromCharCode.apply(null, zip.subarray(i, i + 0x8000)); + return { count: notes.length, files: files.length, base64: btoa(bin) }; + } + case 'import': { + const { settings } = await ctx(); + let imported = 0; + let merged = 0; + const errors = []; + for (const file of args.files || []) { + try { + const incoming = fromMarkdown(file.text); + const existing = await getNote(incoming.id); + await persist(mergeNotes(existing, incoming, new Date()), settings); + existing ? merged++ : imported++; + } catch (e) { + errors.push(`${file.name}: ${e.message}`); + } + } + await rebuildIndex(); + return { imported, merged, errors }; + } + case 'writeAllToVault': + case 'syncVault': { + const r = await syncVault(args); + return { ...r, written: r.report?.written || 0, total: (await getIndex() && Object.keys(await getIndex()).length) }; + } + case 'vaultStatus': + // service workers do not get FileSystemObserver today; the pages watch (see popup.js, options.js) + return { ...(await vaultStatus()), connected: (await vaultFolder()).status !== 'no-vault', workerCanWatch: canObserve(), workerWatching: !!vaultObserver }; + case 'shareExport': { + // one page's stickies (and notes if asked) as a flat text file, no server + const note = await getNote(args.id); + if (!note) throw new Error('Note not found'); + const payload = sharePayload(note, { includeEntries: !!args.includeEntries }); + if (!payload.stickies.length && !payload.entries?.length) throw new Error('Nothing to share on this page yet.'); + return { text: await encodeShare(payload, { passphrase: args.passphrase || '' }), fileName: shareFileName(note), stickies: payload.stickies.length, entries: payload.entries?.length || 0 }; + } + case 'shareImport': { + // received stickies land on the recipient's note for that page, + // by the recipient's own identity rules + const payload = await decodeShare(args.text || '', { passphrase: args.passphrase || '' }); + const { ident, note, settings } = await noteForUrl(payload.url); + const base = note || newNote({ url: payload.url, title: payload.title || payload.url, key: ident.key, rule: ident.rule }); + const { note: next, addedStickies, addedEntries } = applyShare(base, payload); + const saved = next !== base ? await persist(next, settings) : base; + if (next !== base) await notifyLayers(payload.url); + const current = args.url ? identify(args.url, (await ctx()).compiled, settings.normalization).id === ident.id : null; + return { id: saved?.id || base.id, title: saved?.title || base.title, url: payload.url, addedStickies, addedEntries, samePage: current }; + } + case 'widgetLayout': { + // where the user left the floating button and its window; one + // local preference shared by every page + if (args.layout && typeof args.layout === 'object') { + const prev = (await chrome.storage.local.get(WIDGET_KEY))[WIDGET_KEY] || {}; + await chrome.storage.local.set({ [WIDGET_KEY]: { ...prev, ...args.layout } }); + } + return (await chrome.storage.local.get(WIDGET_KEY))[WIDGET_KEY] || {}; + } + case 'settingsChanged': + scheduleBadgeForAll(); + await applyLaunchMode(); + return true; + default: + throw new Error(`Unknown message type ${type}`); + } +} + +// ----- in-page layer -------------------- + +async function layerPresent(tabId) { + try { const r = await chrome.tabs.sendMessage(tabId, { type: 'pn:ping' }); return !!r?.ok; } catch (e) { return false; } +} + +async function ensureLayer(tabId) { + if (await layerPresent(tabId)) return; + await chrome.scripting.executeScript({ target: { tabId }, files: ['src/content/annotate.js'] }); + for (let i = 0; i < 30; i++) { if (await layerPresent(tabId)) return; await new Promise((r) => setTimeout(r, 100)); } + throw new Error('The page did not accept the notes layer.'); +} + +// tell every tab on this page to re-read its stickies (no-op where the layer is absent) +async function notifyLayers(url) { + try { + const { ident, settings } = await noteForUrl(url); + const c = await ctx(); + for (const t of await chrome.tabs.query({})) { + if (!t.url) continue; + if (identify(t.url, c.compiled, settings.normalization).key !== ident.key) continue; + chrome.tabs.sendMessage(t.id, { type: 'pn:refresh' }).catch(() => {}); + } + } catch (e) { /* ignore */ } +} + +async function alwaysShowState(url) { + try { + const origin = new URL(url).origin; + const scripts = await chrome.scripting.getRegisteredContentScripts({ ids: [`layer:${origin}`] }).catch(() => []); + return { origin, enabled: scripts.length > 0 }; + } catch (e) { return { origin: '', enabled: false }; } +} + +// ----- presence badge -------------------- + +const badgeTimers = new Map(); +let allBadgeTimer = null; + +// Badges come from the index (canonical key -> entry count), so a pass over +// every tab costs one settings load, one rule compile and one index read. +async function badgeContext() { + await storageReady; + const c = await ctx(); + const index = await getIndex(); + const byKey = new Map(Object.values(index).map((r) => [r.key, r])); + return { ...c, byKey }; +} + +async function applyBadge(tab, bc) { + try { + if (!bc.settings.showBadge || !tab.url) return chrome.action.setBadgeText({ tabId: tab.id, text: '' }); + const ident = identify(tab.url, bc.compiled, bc.settings.normalization); + const row = bc.byKey.get(ident.key); + const count = (row?.entries || 0) + (row?.stickies || 0); + await chrome.action.setBadgeBackgroundColor({ tabId: tab.id, color: count ? '#c73b3b' : '#6c7189' }); + await chrome.action.setBadgeTextColor({ tabId: tab.id, color: '#ffffff' }); + await chrome.action.setBadgeText({ tabId: tab.id, text: count ? String(count) : row ? '●' : '' }); + await chrome.action.setTitle({ tabId: tab.id, title: count ? `${APP_NAME}: ${count} saved note${count === 1 ? '' : 's'} on this page` : row ? `${APP_NAME}: saved capture` : APP_NAME }); + } catch (e) { /* tab gone */ } +} + +async function updateBadge(tabId) { + try { + const tab = await chrome.tabs.get(tabId); + await applyBadge(tab, await badgeContext()); + } catch (e) { /* tab gone */ } +} + +function scheduleBadge(tabId) { + clearTimeout(badgeTimers.get(tabId)); + badgeTimers.set(tabId, setTimeout(() => { badgeTimers.delete(tabId); updateBadge(tabId); }, 300)); +} + +function scheduleBadgeForAll() { + clearTimeout(allBadgeTimer); + allBadgeTimer = setTimeout(async () => { + allBadgeTimer = null; + try { + const bc = await badgeContext(); + for (const t of await chrome.tabs.query({})) await applyBadge(t, bc); + } catch (e) { /* ignore */ } + }, 300); +} + +chrome.tabs.onActivated.addListener(({ tabId }) => scheduleBadge(tabId)); +chrome.tabs.onUpdated.addListener((tabId, info) => { if (info.url || info.status === 'complete') scheduleBadge(tabId); }); + +// ----- reminders -------------------- + +async function syncReminder(note) { + const name = `remind:${note.id}`; + await chrome.alarms.clear(name); + if (note.remind) { + const when = new Date(note.remind).getTime(); + chrome.alarms.create(name, { when: Math.max(when, Date.now() + 1000) }); + } +} + +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === SYNC_ALARM) { syncVault({ reason: 'alarm' }).catch(() => {}); return; } + if (!alarm.name.startsWith('remind:')) return; + const id = alarm.name.slice('remind:'.length); + const note = await getNote(id); + if (!note) return; + const { settings } = await loadSettings(); + const last = note.entries[note.entries.length - 1]; + try { + await chrome.notifications.create(`note:${id}`, { + type: 'basic', + iconUrl: chrome.runtime.getURL('img/icon128.png'), + title: note.title, + message: last ? last.text.slice(0, 200) : 'Reminder', + priority: 1 + }); + } catch (e) { /* notifications may be disabled */ } + if (settings.openTabOnReminder) { + try { await chrome.tabs.create({ url: note.url, active: true }); } catch (e) { /* ignore */ } + } + await putNote(setReminder(note, null)); + scheduleBadgeForAll(); +}); + +chrome.notifications.onClicked.addListener(async (nid) => { + if (!nid.startsWith('note:')) return; + const note = await getNote(nid.slice(5)); + if (note) chrome.tabs.create({ url: note.url, active: true }); + chrome.notifications.clear(nid); +}); + +// Re-arm alarms after a browser restart (alarms survive, but be safe). +async function rearmReminders() { + const index = await getIndex(); + for (const row of Object.values(index)) { + if (row.remind) { + const existing = await chrome.alarms.get(`remind:${row.id}`); + if (!existing) chrome.alarms.create(`remind:${row.id}`, { when: Math.max(new Date(row.remind).getTime(), Date.now() + 1000) }); + } + } +} + +// ----- context menu and commands -------------------- + +const APP_NAME = chrome.runtime.getManifest().short_name || chrome.runtime.getManifest().name; +const MENU = { addSelection: 'addSelection', snooze: 'snoozeTab' }; +let menuBuild = Promise.resolve(); +function rebuildMenus() { + menuBuild = menuBuild.then(buildMenus, buildMenus); + return menuBuild; +} +async function buildMenus() { + await chrome.contextMenus.removeAll(); + chrome.contextMenus.create({ id: MENU.addSelection, title: 'Add selection to this page\'s note', contexts: ['selection'] }); + chrome.contextMenus.create({ id: MENU.snooze, title: 'Snooze this tab until tomorrow 9:00', contexts: ['page', 'action'] }); +} +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + try { + if (info.menuItemId === MENU.addSelection && info.selectionText) { + const quote = info.selectionText.trim().split('\n').map((l) => `> ${l}`).join('\n'); + await handle({ type: 'addEntry', args: { tabId: tab?.id, url: info.pageUrl || tab?.url, title: tab?.title, text: quote } }); + } else if (info.menuItemId === MENU.snooze) { + await handle({ type: 'snoozeTab', args: { tabId: tab?.id, preset: 'tomorrow' } }); + } + } catch (e) { console.error(e); } +}); +// With no popup set, the icon click reaches us. It is the moment we may read +// the page (activeTab): capture it, then show the note the way the user chose. +// The mode is cached so the side panel can open synchronously: that call only +// works inside the click's gesture, which an awaited storage read can spend. +chrome.action.onClicked.addListener((tab) => { + if (!tab?.id) return; + const web = /^https?:/.test(tab.url || ''); + if (launchMode === 'floating' && !web) { + // browser and extension pages cannot host the layer: the panel instead + chrome.sidePanel.open({ windowId: tab.windowId }).catch((e) => console.error(e)); + return; + } + (async () => { + try { + if (!web) return; + await handle({ type: 'current', args: { tabId: tab.id } }); + if (launchMode !== 'floating') return; + await ensureLayer(tab.id); + await chrome.tabs.sendMessage(tab.id, { type: 'pn:widget', toggle: true }); + } catch (e) { + console.error('launch failed', e); + chrome.action.setBadgeText({ tabId: tab.id, text: '!' }).catch(() => {}); + chrome.action.setTitle({ tabId: tab.id, title: `${APP_NAME}: could not open here (${e.message})` }).catch(() => {}); + } + })(); +}); +chrome.commands.onCommand.addListener(async (command, tab) => { + if (command === 'snoozeTab') { + try { await handle({ type: 'snoozeTab', args: { tabId: tab?.id, preset: 'tomorrow' } }); } catch (e) { console.error(e); } + } +}); + +// ----- launch mode -------------------- + +// The toolbar icon opens a popup, toggles the side panel, or (with no popup +// and no panel behaviour) reaches onClicked, which puts the widget on the +// page. The popup must be cleared for the other two modes: a popup takes the +// click before anything else sees it. +let launchMode = 'popup'; +async function applyLaunchMode() { + const { settings } = await loadSettings(); + const mode = launchMode = settings.launch; + try { await chrome.action.setPopup({ popup: mode === 'popup' ? 'src/popup.html' : '' }); } catch (e) { /* ignore */ } + try { await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: mode === 'sidebar' }); } catch (e) { /* older browsers */ } +} + +// ----- lifecycle -------------------- + +chrome.runtime.onInstalled.addListener(async (details) => { + await rebuildMenus(); + await rearmReminders(); + await applyLaunchMode(); + await scheduleVaultSync(); + scheduleBadgeForAll(); + if (details.reason === 'install') chrome.runtime.openOptionsPage(); +}); +chrome.runtime.onStartup.addListener(() => { rebuildMenus(); rearmReminders(); applyLaunchMode(); scheduleVaultSync(); scheduleBadgeForAll(); syncVault({ reason: 'startup' }).catch(() => {}); }); +scheduleBadgeForAll(); +// the worker restarts often: warm the launch mode so the icon click can act at +// once, and re-arm the folder watch that stopped with the previous worker +applyLaunchMode(); +watchVault(); diff --git a/src/content/annotate.js b/src/content/annotate.js new file mode 100644 index 0000000..c0378f2 --- /dev/null +++ b/src/content/annotate.js @@ -0,0 +1,17 @@ +// Bootstrap injected into the page (classic script). It loads the real layer +// as a module so it can share src/lib/anchor.js with the rest of the extension. +// +// A layer left behind by an earlier version of the extension (reloaded at +// chrome://extensions while this tab stayed open) still owns the window +// global but can no longer reach the worker. Replace it rather than reuse it. +(function () { + const existing = window.__pageNotesLayer; + if (existing && (!existing.alive || existing.alive())) { + existing.refresh(); + return; + } + window.__pageNotesLayer = { refresh() {}, alive: () => true }; + import(chrome.runtime.getURL('src/content/layer.js')) + .then((m) => { window.__pageNotesLayer = m.boot(); }) + .catch((e) => { window.__pageNotesLayer = null; console.warn('page notes layer failed', e); }); +})(); diff --git a/src/content/layer.js b/src/content/layer.js new file mode 100644 index 0000000..acc5c6a --- /dev/null +++ b/src/content/layer.js @@ -0,0 +1,455 @@ +// The in-page layer: numbered pins on annotated elements, a popover to read +// and edit, and a pick mode to add a sticky to any element. Everything lives +// in a shadow root so page styles and ours never touch. + +import { describe, resolve } from '../lib/anchor.js'; + +const APP = chrome.runtime.getManifest().short_name || chrome.runtime.getManifest().name; +// "Hacktoolkit" from the manifest author, "Hacktoolkit (@hacktoolkit)" +const BY = (chrome.runtime.getManifest().author || '').replace(/\s*\(.*\)\s*$/, '').trim(); + +const CSS = ` +:host { all: initial; } +[hidden] { display: none !important; } +* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } +.pin { position: absolute; z-index: 2147483000; width: 28px; height: 28px; padding: 0; border: 1px solid #bc9138; border-radius: 5px 5px 10px 5px; background: #f9d97e; color: #51390c; font: 700 12px/26px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: center; box-shadow: 0 3px 8px rgba(62,44,9,.25), 0 0 0 2px #fff9e8; cursor: pointer; user-select: none; transition: transform .12s ease; } +.pin::after { content: ''; position: absolute; bottom: 0; right: 0; width: 7px; height: 7px; border-radius: 3px 0 8px 0; background: #c89a3c; } +.pin:hover { transform: scale(1.12); } +.pin.orphan { background: #b7791f; } +.pin.open { background: #ffe9a7; transform: rotate(-4deg) scale(1.1); } +.pin:focus-visible, button:focus-visible { outline: 3px solid #4f5bd5; outline-offset: 3px; } +.ring { position: absolute; z-index: 2147482999; pointer-events: none; border: 2px solid #c4922b; border-radius: 6px; box-shadow: 0 0 0 4px rgba(224,168,74,.15); } +.ring.pick { border-color: #4f5bd5; background: rgba(79,91,213,.07); } +.pop { position: absolute; z-index: 2147483001; width: min(300px, calc(100vw - 16px)); background: #fff9e8; color: #30240f; border-radius: 6px 6px 20px 6px; box-shadow: 0 2px 4px rgba(62,44,9,.08), 0 20px 48px rgba(62,44,9,.22); border: 1px solid #e8cb7e; border-top: 5px solid #f4cd6b; padding: 12px 14px; } +.pop .label { font-size: 10px; letter-spacing: .09em; text-transform: uppercase; font-weight: 700; color: #886622; margin-bottom: 10px; } +.pop .q { font-size: 11px; color: #6c7189; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 6px; } +.pop .q b { color: #886622; margin-right: 6px; } +.pop textarea { width: 100%; min-height: 64px; resize: none; border: 0; padding: 0; font-size: 13px; line-height: 1.45; color: #17192a; background: transparent; outline: none; } +.pop .row { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e3e5ee; font-size: 11px; color: #6c7189; } +.pop .row .sp { flex: 1; } +.pop button { font: inherit; border: 0; background: transparent; color: #6c7189; cursor: pointer; padding: 2px 6px; border-radius: 6px; } +.pop button:hover { background: #eff0f6; color: #17192a; } +.pop button.del:hover { color: #c73b3b; } +.hint { position: fixed; z-index: 2147483002; left: 50%; top: 16px; transform: translateX(-50%); background: #17192a; color: #f6f6fa; font-size: 12px; padding: 8px 14px; border-radius: 999px; box-shadow: 0 8px 24px rgba(0,0,0,.25); display: flex; gap: 12px; align-items: center; } +.hint button { font: inherit; border: 0; background: rgba(255,255,255,.14); color: inherit; border-radius: 999px; padding: 2px 10px; cursor: pointer; } +.dock { position: fixed; bottom: 18px; right: 18px; z-index: 2147483002; display: flex; align-items: center; gap: 10px; background: #fff9e8; color: #51390c; border: 1px solid #e8cb7e; border-radius: 12px; padding: 8px 10px; box-shadow: 0 4px 18px rgba(62,44,9,.18); font-size: 12px; } +.dock .mark { background: #f4cd6b; padding: 3px 6px; border-radius: 3px 3px 7px 3px; font-weight: 700; } +.dock button { border: 0; border-left: 1px solid #e8cb7e; padding: 4px 2px 4px 10px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-weight: 600; } +.dock.with-fab { right: 74px; } +.fab { position: fixed; z-index: 2147483003; width: 44px; height: 44px; padding: 0; border: 1px solid #bc9138; border-radius: 14px 14px 22px 14px; background: #f9d97e; box-shadow: 0 3px 8px rgba(62,44,9,.25), 0 0 0 2px #fff9e8; cursor: grab; display: flex; align-items: center; justify-content: center; touch-action: none; user-select: none; transition: transform .12s ease, box-shadow .12s ease; } +.fab::after { content: ''; position: absolute; bottom: 0; right: 0; width: 11px; height: 11px; border-radius: 4px 0 14px 0; background: #c89a3c; } +.fab img { width: 26px; height: 26px; pointer-events: none; } +.fab .glyph { font-size: 20px; line-height: 1; color: #51390c; pointer-events: none; } +.fab:hover { transform: scale(1.06); } +.fab.dragging { cursor: grabbing; transform: scale(1.08); box-shadow: 0 10px 24px rgba(62,44,9,.3), 0 0 0 2px #fff9e8; } +.fab.open { box-shadow: 0 3px 8px rgba(62,44,9,.25), 0 0 0 3px #4f5bd5; } +.fab .count { position: absolute; top: -7px; left: -7px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: #c73b3b; color: #fff; font: 700 11px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: center; } +.widget { position: fixed; z-index: 2147483004; width: min(384px, calc(100vw - 16px)); height: min(620px, calc(100vh - 24px)); display: flex; flex-direction: column; overflow: hidden; background: #f6f6fa; color: #17192a; border: 1px solid #d3d6e3; border-radius: 14px; box-shadow: 0 2px 4px rgba(23,25,42,.06), 0 24px 56px rgba(23,25,42,.28); } +.widget .bar { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 6px 0 12px; background: #eff0f6; border-bottom: 1px solid #e3e5ee; color: #6c7189; font: 600 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; letter-spacing: .04em; text-transform: uppercase; cursor: grab; user-select: none; touch-action: none; } +.widget .bar .grip { color: #b7bbcf; letter-spacing: -2px; font-size: 14px; } +.widget .bar .by { text-transform: none; letter-spacing: 0; font-weight: 500; opacity: .75; margin-left: 2px; } +.widget .bar .sp { flex: 1; } +.widget .bar button { font: inherit; font-size: 14px; line-height: 1; border: 0; background: transparent; color: #6c7189; cursor: pointer; padding: 4px 7px; border-radius: 6px; } +.widget .bar button:hover { background: #e3e5ee; color: #17192a; } +.widget.dragging .bar { cursor: grabbing; } +.widget iframe { flex: 1; width: 100%; border: 0; background: transparent; color-scheme: light dark; } +@media (prefers-color-scheme: dark) { + .widget { background: #13141c; color: #eceef6; border-color: #383c52; } + .widget .bar { background: #1b1d29; border-color: #2a2d3e; color: #9397ae; } + .widget .bar .grip { color: #4a4e66; } + .widget .bar button { color: #9397ae; } + .widget .bar button:hover { background: #2c3043; color: #eceef6; } + .pop, .dock { background: #302a1e; color: #f4e7c7; border-color: #6d5830; } + .pop textarea { color: #f4e7c7; } + .pop .q, .pop .row, .pop button { color: #c4b58f; } + .pop .label, .pop .q b { color: #f1ce7d; } + .pop .row { border-color: #6d5830; } + .pop button:hover { background: #443923; color: #f4e7c7; } + .dock .mark { color: #51390c; } +} +@media (prefers-reduced-motion: reduce) { * { transition: none !important; } } +`; + +function send(type, args = {}) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type, args }, (r) => { + if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message)); + else if (!r || !r.ok) reject(new Error(r?.error || 'No response')); + else resolve(r.result); + }); + }); +} + +export function boot() { + // a host left by an earlier extension instance is dead: start clean + for (const stale of document.querySelectorAll('[data-page-notes]')) stale.remove(); + const host = document.createElement('div'); + host.setAttribute('data-page-notes', ''); + host.style.cssText = 'position:absolute;left:0;top:0;width:0;height:0;overflow:visible;z-index:2147483000;'; + const shadow = host.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = CSS; + shadow.append(style); + const annotations = document.createElement('div'); + annotations.className = 'annotations'; + annotations.hidden = true; + const dock = document.createElement('div'); dock.className = 'dock'; dock.hidden = true; + const mark = document.createElement('span'); mark.className = 'mark'; + const dockLabel = document.createElement('span'); dockLabel.textContent = APP; + const toggle = document.createElement('button'); + dock.append(mark, dockLabel, toggle); + // the floating button and the window it opens (launch mode "floating") + const fab = document.createElement('button'); + fab.className = 'fab'; fab.hidden = true; fab.title = `${APP}: notes for this page (drag to move)`; + fab.setAttribute('aria-label', `${APP}: open notes for this page`); + const fabIcon = document.createElement('img'); fabIcon.alt = ''; fabIcon.src = chrome.runtime.getURL('img/icon32.png'); + fabIcon.addEventListener('error', () => { fabIcon.replaceWith(Object.assign(document.createElement('span'), { className: 'glyph', textContent: '✎' })); }); + const fabCount = document.createElement('span'); fabCount.className = 'count'; fabCount.hidden = true; + fab.append(fabIcon, fabCount); + shadow.append(annotations, dock, fab); + document.documentElement.append(host); + + let stickies = []; + let placed = new Map(); // sticky id -> { el, pin } + let pop = null; + let picking = null; + let visible = true; + let visibilityVersion = 0; + + function setVisible(value) { + visible = value; + if (!visible) { exitPick(); closePop(); } + annotations.hidden = !visible; + dock.hidden = stickies.length === 0; + mark.textContent = String(stickies.length); + updateFabCount(stickies.length); + toggle.textContent = visible ? 'Hide stickies' : 'Show stickies'; + toggle.setAttribute('aria-pressed', String(visible)); + layout(); + } + async function reveal() { + const result = await send('stickyVisibility', { visible: true }); + setVisible(result.visible); + } + toggle.addEventListener('click', async () => { + try { const result = await send('stickyVisibility', { visible: !visible }); setVisible(result.visible); } + catch (e) { toggle.textContent = 'Try again'; toggle.title = e.message; } + }); + + const isOurs = (el) => host.contains(el) || el === host; + + function rectOf(el) { + const r = el.getBoundingClientRect(); + return { left: r.left + window.scrollX, top: r.top + window.scrollY, width: r.width, height: r.height }; + } + + function layout() { + for (const { el, pin, ring } of placed.values()) { + if (!el) continue; // Orphans live in a fixed tray above the control. + if (!el.isConnected) { pin.style.display = 'none'; if (ring) ring.style.display = 'none'; continue; } + const r = rectOf(el); + pin.style.display = ''; + pin.style.left = `${Math.max(window.scrollX, Math.min(r.left + r.width - 14, window.scrollX + window.innerWidth - 30))}px`; + pin.style.top = `${Math.max(0, r.top - 14)}px`; + // the ring is positioned here but shown only on hover or while open + if (ring) Object.assign(ring.style, { left: `${r.left - 3}px`, top: `${r.top - 3}px`, width: `${r.width + 6}px`, height: `${r.height + 6}px` }); + } + if (pop?.anchorEl) { + const r = rectOf(pop.anchorEl); + pop.style.left = `${Math.max(window.scrollX + 8, Math.min(r.left, window.scrollX + window.innerWidth - 316))}px`; + pop.style.top = `${r.top + r.height + 10}px`; + } + } + + function closePop() { + if (pop) { pop.flush?.(); pop.remove(); pop = null; } + for (const { pin, ring } of placed.values()) { pin.classList.remove('open'); if (ring) ring.style.display = 'none'; } + } + + function openPop(sticky, el, index) { + closePop(); + pop = document.createElement('div'); + pop.className = 'pop'; + pop.anchorEl = el; + const label = document.createElement('div'); label.className = 'label'; label.textContent = `${APP} · Sticky ${index}`; + const q = document.createElement('div'); + q.className = 'q'; + q.innerHTML = `${index}`; + q.append(sticky.anchor?.quote?.exact ? `“${sticky.anchor.quote.exact.slice(0, 80)}”` : (sticky.anchor?.label || sticky.anchor?.tag || 'element')); + const ta = document.createElement('textarea'); + ta.placeholder = 'What about this part? Saves as you type.'; + ta.value = sticky.text || ''; + let timer = null; + const save = async () => { + const text = ta.value; + if (text === sticky.text) return; + try { await send('stickyUpdate', { url: location.href, id: sticky.id, text }); sticky.text = text; status.textContent = 'Saved'; } catch (e) { status.textContent = e.message; } + }; + ta.addEventListener('input', () => { status.textContent = 'Saving…'; clearTimeout(timer); timer = setTimeout(save, 400); }); + ta.addEventListener('blur', () => { clearTimeout(timer); save(); }); + const row = document.createElement('div'); + row.className = 'row'; + const status = document.createElement('span'); + const sp = document.createElement('span'); sp.className = 'sp'; + const del = document.createElement('button'); del.className = 'del'; del.textContent = 'Delete'; + del.addEventListener('click', async () => { clearTimeout(timer); if (pop) pop.flush = null; await send('stickyRemove', { url: location.href, id: sticky.id }); await refresh(); }); + const done = document.createElement('button'); done.textContent = 'Done'; + done.addEventListener('click', async () => { clearTimeout(timer); await save(); closePop(); }); + row.append(status, sp, del, done); + pop.flush = () => { clearTimeout(timer); return save(); }; + pop.append(label, q, ta, row); + annotations.append(pop); + const entry = placed.get(sticky.id); + entry?.pin.classList.add('open'); + if (entry?.ring) entry.ring.style.display = ''; + layout(); + ta.focus(); + } + + function render() { + for (const { pin, ring } of placed.values()) { pin.remove(); ring?.remove(); } + placed = new Map(); + stickies.forEach((s, i) => { + const { el, how } = resolve(s.anchor, document); + const pin = document.createElement('button'); + pin.className = `pin${how === 'orphan' ? ' orphan' : ''}`; + pin.textContent = String(i + 1); + pin.title = s.text || 'Sticky'; + pin.setAttribute('aria-label', `Sticky ${i + 1}: ${s.text || 'No comment yet'}`); + let ring = null; + if (el) { + ring = document.createElement('div'); + ring.className = 'ring'; + ring.style.display = 'none'; + pin.addEventListener('mouseenter', () => { ring.style.display = ''; layout(); }); + pin.addEventListener('mouseleave', () => { if (!pop || pop.anchorEl !== el) ring.style.display = 'none'; }); + pin.addEventListener('click', (e) => { e.stopPropagation(); openPop(s, el, i + 1); }); + annotations.append(ring); + } else { + pin.style.position = 'fixed'; + pin.style.right = `${12 + i * 34}px`; + pin.style.bottom = '74px'; + pin.title = `${s.text || 'Sticky'} (could not find its place on this page)`; + pin.addEventListener('click', () => { openPop(s, null, i + 1); pop.style.position = 'fixed'; pop.style.right = '16px'; pop.style.bottom = '112px'; }); + } + annotations.append(pin); + placed.set(s.id, { el, pin, ring }); + }); + setVisible(visible); + send('stickyPlacement', { url: location.href, placement: stickies.map((s) => ({ id: s.id, found: !!placed.get(s.id)?.el })) }).catch(() => {}); + } + + async function refresh() { + if (pop?.flush) await pop.flush(); + try { stickies = await send('stickies', { url: location.href }); } catch (e) { stickies = []; } + closePop(); + render(); + } + + // ----- floating widget -------------------- + // The note itself is the extension's own panel page in an iframe, so the + // page we sit on can neither read nor style it. The layer only places the + // frame and the button, and remembers where the user dragged them. + let widget = null, frame = null; + let layoutState = {}; // { fab: {x, y}, widget: {x, y} } in viewport px + const extensionOrigin = new URL(chrome.runtime.getURL('')).origin; + const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); + + function placeFab() { + const w = fab.offsetWidth || 44, h = fab.offsetHeight || 44; + const pos = layoutState.fab || { x: window.innerWidth - w - 18, y: window.innerHeight - h - 18 }; + const x = clamp(pos.x, 4, window.innerWidth - w - 4), y = clamp(pos.y, 4, window.innerHeight - h - 4); + fab.style.left = `${x}px`; fab.style.top = `${y}px`; + } + function placeWidget() { + if (!widget || widget.hidden) return; + const w = widget.offsetWidth || 384, h = widget.offsetHeight || 620; + let pos = layoutState.widget; + if (!pos) { + // next to the button: above it when it sits low, below when high, + // right-aligned on the right half of the viewport + const f = fab.getBoundingClientRect(); + const x = f.left + f.width / 2 > window.innerWidth / 2 ? f.right - w : f.left; + const y = f.top + f.height / 2 > window.innerHeight / 2 ? f.top - h - 10 : f.bottom + 10; + pos = { x, y }; + } + widget.style.left = `${clamp(pos.x, 4, Math.max(4, window.innerWidth - w - 4))}px`; + widget.style.top = `${clamp(pos.y, 4, Math.max(4, window.innerHeight - h - 4))}px`; + } + function saveLayout(patch) { + layoutState = { ...layoutState, ...patch }; + send('widgetLayout', { layout: patch }).catch(() => {}); + } + // Drag by pointer; a press that barely moves is a click. + function draggable(el, handle, key, onClick) { + let start = null; + handle.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + const r = el.getBoundingClientRect(); + start = { x: e.clientX, y: e.clientY, left: r.left, top: r.top, moved: false }; + try { handle.setPointerCapture(e.pointerId); } catch (err) { /* synthetic pointer */ } + }); + handle.addEventListener('pointermove', (e) => { + if (!start) return; + const dx = e.clientX - start.x, dy = e.clientY - start.y; + if (!start.moved && Math.hypot(dx, dy) < 4) return; + start.moved = true; + el.classList.add('dragging'); + const w = el.offsetWidth, h = el.offsetHeight; + el.style.left = `${clamp(start.left + dx, 4, window.innerWidth - w - 4)}px`; + el.style.top = `${clamp(start.top + dy, 4, window.innerHeight - h - 4)}px`; + if (el === fab && !layoutState.widget) placeWidget(); + }); + const finish = (e) => { + if (!start) return; + const moved = start.moved; + start = null; + el.classList.remove('dragging'); + try { handle.releasePointerCapture(e.pointerId); } catch (err) { /* already released */ } + if (moved) saveLayout({ [key]: { x: parseFloat(el.style.left), y: parseFloat(el.style.top) } }); + else if (onClick) onClick(); + }; + handle.addEventListener('pointerup', finish); + handle.addEventListener('pointercancel', finish); + } + function showFab() { + if (!fab.hidden) return; + fab.hidden = false; + dock.classList.add('with-fab'); + placeFab(); + } + function updateFabCount(n) { + fabCount.hidden = !n; + fabCount.textContent = String(n); + } + function openWidget() { + if (!widget) { + widget = document.createElement('div'); + widget.className = 'widget'; + widget.setAttribute('role', 'dialog'); + widget.setAttribute('aria-label', `${APP}${BY ? ` by ${BY}` : ''} for this page`); + const bar = document.createElement('div'); bar.className = 'bar'; + const grip = document.createElement('span'); grip.className = 'grip'; grip.textContent = '⋮⋮'; grip.setAttribute('aria-hidden', 'true'); + const title = document.createElement('span'); title.textContent = APP; + if (BY) { const by = document.createElement('span'); by.className = 'by'; by.textContent = ` by ${BY}`; title.append(by); } + const sp = document.createElement('span'); sp.className = 'sp'; + const close = document.createElement('button'); close.textContent = '✕'; close.title = 'Close (Esc)'; close.setAttribute('aria-label', 'Close notes'); + close.addEventListener('click', closeWidget); + bar.append(grip, title, sp, close); + frame = document.createElement('iframe'); + frame.src = `${chrome.runtime.getURL('src/panel.html')}?embed=1`; + frame.title = `${APP} for this page`; + frame.allow = 'clipboard-write'; + widget.append(bar, frame); + shadow.append(widget); + draggable(widget, bar, 'widget'); + } else { + widget.hidden = false; + frame.contentWindow?.postMessage({ type: 'pn:shown' }, extensionOrigin); + } + fab.classList.add('open'); + placeWidget(); + frame.focus(); + } + function closeWidget() { + if (!widget || widget.hidden) return; + widget.hidden = true; + fab.classList.remove('open'); + fab.focus(); + } + function toggleWidget() { if (widget && !widget.hidden) closeWidget(); else openWidget(); } + draggable(fab, fab, 'fab', toggleWidget); + window.addEventListener('message', (e) => { + if (!frame || e.source !== frame.contentWindow || e.origin !== extensionOrigin) return; + if (e.data?.type === 'pn:widget-close') closeWidget(); + }); + window.addEventListener('resize', () => { if (!fab.hidden) placeFab(); placeWidget(); }); + + // ----- pick mode -------------------- + async function enterPick() { + await reveal(); + if (picking) return; + const ring = document.createElement('div'); + ring.className = 'ring pick'; + ring.style.display = 'none'; + const hint = document.createElement('div'); + hint.className = 'hint'; + hint.append(`Click the part of the page you want to comment on`); + const cancel = document.createElement('button'); + cancel.textContent = 'Esc to cancel'; + cancel.addEventListener('click', exitPick); + hint.append(cancel); + shadow.append(ring, hint); + let target = null; + const onMove = (e) => { + // skip our own hint and ring, which may sit over the page + const el = (document.elementsFromPoint(e.clientX, e.clientY) || []).find((x) => !isOurs(x)); + if (!el || el === document.documentElement || el === document.body) { ring.style.display = 'none'; target = null; return; } + target = el; + const r = rectOf(el); + ring.style.display = ''; + Object.assign(ring.style, { left: `${r.left - 3}px`, top: `${r.top - 3}px`, width: `${r.width + 6}px`, height: `${r.height + 6}px` }); + }; + const onClick = async (e) => { + if (isOurs(e.target)) return; + if (!target) return; + e.preventDefault(); + e.stopPropagation(); + const anchor = describe(target, document); + exitPick(); + try { + const s = await send('stickyAdd', { url: location.href, anchor }); + await refresh(); + const placedEntry = placed.get(s.id); + if (placedEntry?.el) openPop(s, placedEntry.el, stickies.findIndex((x) => x.id === s.id) + 1); + } catch (err) { console.warn(err); } + }; + const onKey = (e) => { if (e.key === 'Escape') exitPick(); }; + document.addEventListener('mousemove', onMove, true); + document.addEventListener('click', onClick, true); + document.addEventListener('keydown', onKey, true); + document.documentElement.style.cursor = 'crosshair'; + picking = { ring, hint, onMove, onClick, onKey }; + } + function exitPick() { + if (!picking) return; + document.removeEventListener('mousemove', picking.onMove, true); + document.removeEventListener('click', picking.onClick, true); + document.removeEventListener('keydown', picking.onKey, true); + document.documentElement.style.cursor = ''; + picking.ring.remove(); + picking.hint.remove(); + picking = null; + } + + async function scrollTo(id) { + await reveal(); + const p = placed.get(id); + if (p?.el) { p.el.scrollIntoView({ block: 'center', behavior: 'smooth' }); const s = stickies.find((x) => x.id === id); setTimeout(() => openPop(s, p.el, stickies.indexOf(s) + 1), 300); } + } + + chrome.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg?.type === 'pn:refresh') refresh(); + else if (msg?.type === 'pn:pick') enterPick(); + else if (msg?.type === 'pn:scrollTo') scrollTo(msg.id); + else if (msg?.type === 'pn:visibility') { ++visibilityVersion; setVisible(msg.visible); } + else if (msg?.type === 'pn:widget') { + // the toolbar click in floating mode: show the button, open or toggle the window + const ready = Object.keys(layoutState).length ? Promise.resolve() : send('widgetLayout').then((l) => { layoutState = l && typeof l === 'object' ? l : {}; }, () => {}); + ready.then(() => { showFab(); if (msg.toggle) toggleWidget(); else openWidget(); }); + } + else if (msg?.type === 'pn:ping') { respond({ ok: true }); return; } + respond({ ok: true }); + }); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && widget && !widget.hidden && !picking) closeWidget(); }, true); + window.addEventListener('scroll', layout, { passive: true }); + window.addEventListener('resize', layout); + new MutationObserver(() => { clearTimeout(boot._t); boot._t = setTimeout(layout, 100); }).observe(document.body, { childList: true, subtree: true, attributes: false }); + document.addEventListener('click', (e) => { if (pop && !isOurs(e.target)) closePop(); }, true); + + const version = visibilityVersion; + send('stickyVisibility').then((r) => { if (version === visibilityVersion) setVisible(r.visible); }).then(refresh).catch(() => {}); + // false once the extension that created us was reloaded or updated + const alive = () => { try { return !!chrome.runtime?.id; } catch (e) { return false; } }; + return { refresh, enterPick, scrollTo, openWidget, closeWidget, alive, app: APP }; +} diff --git a/src/explorer.css b/src/explorer.css new file mode 100644 index 0000000..7f8b215 --- /dev/null +++ b/src/explorer.css @@ -0,0 +1,82 @@ +.explorer { container-type: inline-size; min-width: 0; } +.explorer-search { display: grid; gap: 8px; margin-bottom: 12px; } +.explorer-search input[type="search"] { width: 100%; } +.explorer-search label { display: flex; align-items: center; gap: 6px; font-size: var(--fs-s); color: var(--muted); } +.explorer-trail { display: flex; align-items: center; gap: 4px; padding: 6px 0; color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; overflow: hidden; min-height: 30px; } +.explorer-trail .crumb { border: 0; background: transparent; color: var(--accent-text); padding: 3px 6px; border-radius: 6px; font-size: var(--fs-xs); box-shadow: none; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.explorer-trail button.crumb:hover { background: var(--accent-soft); } +.explorer-trail .crumb[aria-current="page"] { color: var(--text); font-weight: 600; cursor: default; background: transparent; } +.explorer-trail .crumb.current { flex: 1; min-width: 0; max-width: none; } +.explorer-trail .sep { color: var(--line-2); } +.explorer-columns { display: grid; grid-template-columns: minmax(140px, .65fr) minmax(210px, 1fr) minmax(280px, 1.5fr); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel); height: min(660px, 72vh); min-height: 420px; } +.explorer-column { min-width: 0; overflow: auto; border-right: 1px solid var(--line); background: var(--bg); } +.explorer-column > summary { padding: 12px 14px; font-size: var(--fs-xs); font-weight: 650; color: var(--muted); border-bottom: 1px solid var(--line); cursor: pointer; position: sticky; top: 0; background: var(--bg); z-index: 1; } +.explorer-list { padding: 6px; display: grid; gap: 3px; } +.explorer-list button { text-align: left; border: 1px solid transparent; background: transparent; width: 100%; min-width: 0; padding: 10px; box-shadow: none; } +.explorer-list button:hover { background: var(--panel-2); } +.explorer-list button.selected { background: var(--accent-soft); color: var(--accent-text); border-color: var(--line-2); } +.explorer-site { display: flex; gap: 8px; align-items: center; } +.explorer-folder { color: var(--accent); font-size: 19px; line-height: 1; } +.explorer-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-s); } +.explorer-count { font-size: var(--fs-xs); color: var(--muted); } +.explorer-group { display: flex; gap: 8px; align-items: center; padding: 12px 10px 4px; font-size: var(--fs-xs); font-weight: 650; color: var(--muted); } +.explorer-group:first-child { padding-top: 4px; } +.explorer-group .explorer-folder { font-size: 15px; } +.explorer-group .explorer-label { font-size: var(--fs-xs); text-transform: none; } +.explorer-group + button { margin-left: 0; } +.explorer-page { display: grid; gap: 4px; } +.explorer-page-title { font-weight: 600; font-size: var(--fs-m); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } +.explorer-page-meta, .explorer-page-excerpt { font-size: var(--fs-xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.explorer-link-count { display: inline-flex; align-items: center; gap: 3px; margin-left: 8px; padding: 0 6px; border-radius: 999px; background: var(--accent-soft); color: var(--accent-text); font-weight: 600; vertical-align: 1px; } +.explorer-link-count .mark { font-size: 10px; } +.explorer-list button.selected .explorer-link-count { background: var(--panel); } +.explorer-preview { overflow: auto; min-width: 0; padding: 20px; } +.explorer-preview h3 { font-size: 20px; line-height: 1.25; color: var(--text); margin: 8px 0; letter-spacing: -.025em; text-transform: none; overflow-wrap: anywhere; } +.explorer-url { color: var(--muted); font-size: var(--fs-xs); overflow-wrap: anywhere; } +.explorer-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 16px 0 20px; } +.explorer-actions button { font-size: var(--fs-s); padding: 5px 9px; } +.explorer-preview h4 { font-size: 10px; text-transform: uppercase; letter-spacing: .09em; color: var(--muted); margin: 20px 0 8px; } +.explorer-item { margin-bottom: 8px; border: 1px solid var(--line); border-radius: var(--radius-s); overflow: hidden; } +.explorer-item summary { padding: 10px 12px; cursor: pointer; font-size: var(--fs-xs); } +.explorer-item-label { font-weight: 600; } +.explorer-item-excerpt { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); margin-top: 3px; } +.explorer-item[open] .explorer-item-excerpt { display: none; } +.explorer-body { padding: 0 12px 12px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; font-size: var(--fs-m); } +.explorer-body p { margin: 8px 0 0; } +.explorer-body blockquote { margin: 0; padding-left: 10px; border-left: 2px solid var(--sticky-edge); color: var(--muted); font-size: var(--fs-s); } +.explorer-item.sticky { background: var(--sticky-soft); border-color: var(--sticky-edge); } +.explorer-item.capture .explorer-body { max-height: 240px; overflow: auto; font-size: var(--fs-s); } +.explorer-bottom { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; margin-top: 24px; color: var(--muted); font-size: var(--fs-xs); } +.explorer-bottom button { padding: 3px 6px; font-size: var(--fs-xs); } +.explorer-empty { padding: 32px 4px; color: var(--muted); line-height: 1.6; } +.explorer-empty b { font-size: var(--fs-l); color: var(--text); } +.explorer-message { padding: 10px; border-radius: var(--radius-s); background: var(--danger-soft); color: var(--danger-text); } +@container (min-width: 700px) { + .explorer-column > summary { list-style: none; cursor: default; } +} +@container (max-width: 699px) { + /* one level at a time: sites, then the chosen site's pages, then the page */ + .explorer-columns { display: flex; flex-direction: column; height: auto; min-height: 0; } + .explorer-column { border-right: 0; flex-shrink: 0; } + .explorer-column > summary { position: static; list-style: none; cursor: default; } + .explorer-list { max-height: none; } + .explorer-preview { padding: 16px; overflow: visible; } + .explorer-preview h3 { font-size: 18px; } + .explorer[data-level="0"] .explorer-pages, .explorer[data-level="0"] .explorer-preview, + .explorer[data-level="1"] .explorer-sites, .explorer[data-level="1"] .explorer-preview, + .explorer[data-level="2"] .explorer-sites, .explorer[data-level="2"] .explorer-pages { display: none; } +} + +/* linked pages inside the preview */ +.explorer-links h4 { display: flex; align-items: center; gap: 10px; } +.explorer-links h4 button { font-size: var(--fs-xs); padding: 2px 8px; border-radius: 999px; text-transform: none; letter-spacing: 0; font-weight: 500; } +.explorer-picker { margin: 0 0 10px; } +.explorer-link-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; } +.explorer-link-list li { display: flex; align-items: center; gap: 6px; } +.explorer-link-list li.muted { font-size: var(--fs-s); line-height: 1.5; } +.explorer-link { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; text-align: left; padding: 7px 10px; border: 1px solid var(--line); background: var(--bg); box-shadow: none; } +.explorer-link:hover { border-color: var(--accent); } +.explorer-link .host { font-size: var(--fs-xs); color: var(--accent-text); background: var(--accent-soft); padding: 1px 7px; border-radius: 999px; white-space: nowrap; } +.explorer-link .title { font-size: var(--fs-s); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.explorer-link-list .tiny { font-size: var(--fs-xs); padding: 2px 8px; } +.explorer-link-list li.missing .title { color: var(--muted); text-decoration: line-through; } diff --git a/src/lib/anchor.js b/src/lib/anchor.js new file mode 100644 index 0000000..73e2ac1 --- /dev/null +++ b/src/lib/anchor.js @@ -0,0 +1,164 @@ +// Anchors: how a sticky finds its element again. Pure over a minimal DOM +// surface (tagName, id, parentElement, children, textContent, getAttribute, +// querySelector/querySelectorAll on the root) so it runs in the page and in +// node tests with fake nodes. +// +// An anchor keeps three locators and resolves in order: +// 1. css a selector rooted at the nearest ancestor with a stable id, +// verified by text similarity; +// 2. quote the element's text with a little before and after (W3C +// TextQuoteSelector), found by searching the document; +// 3. path a structural tag:nth-of-type chain from the root, last resort. +// Anything that fails verification is reported as orphaned, never guessed. + +const QUOTE_MAX = 200; +const CONTEXT = 32; +const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'PATH']); + +export function normalizeText(s) { + return (s || '').replace(/\s+/g, ' ').trim(); +} + +// ids that look generated (hashes, numbers, framework suffixes) are not stable +export function isStableId(id) { + if (!id || id.length > 64) return false; + if (!/^[A-Za-z_][\w:.-]*$/.test(id)) return false; + if (/\d{5,}/.test(id)) return false; + if (/^(?:[a-f0-9]{8,}|[A-Za-z0-9]{20,})$/.test(id)) return false; + if (/^(?:ember|react|mui|radix|headlessui|rc_|__|:r)/.test(id)) return false; + return true; +} + +function cssEscape(s) { + return s.replace(/([^\w-])/g, '\\$1'); +} + +function nthOfType(el) { + const parent = el.parentElement; + if (!parent) return 1; + let n = 0; + for (const sib of parent.children) { + if (sib.tagName === el.tagName) n++; + if (sib === el) return n; + } + return n || 1; +} + +function stepFor(el) { + return `${el.tagName.toLowerCase()}:nth-of-type(${nthOfType(el)})`; +} + +// Selector from the nearest stable-id ancestor (or the root) down to el. +export function cssPathFor(el) { + const steps = []; + let node = el; + while (node && node.tagName && node.tagName !== 'HTML') { + if (node.id && isStableId(node.id)) { + steps.unshift(`#${cssEscape(node.id)}`); + return steps.join(' > '); + } + steps.unshift(node.tagName === 'BODY' ? 'body' : stepFor(node)); + node = node.parentElement; + } + return steps.join(' > '); +} + +export function structuralPathFor(el) { + const steps = []; + let node = el; + while (node && node.tagName && node.tagName !== 'HTML') { + steps.unshift(node.tagName === 'BODY' ? 'body' : stepFor(node)); + node = node.parentElement; + } + return steps.join(' > '); +} + +// Text before and after the element in document order, for the quote. +function contextAround(el, root) { + const all = normalizeText(root.textContent || ''); + const own = normalizeText(el.textContent || ''); + if (!own) return { prefix: '', suffix: '' }; + // the element's text may appear more than once; use the occurrence that + // matches its position among equal texts + let occurrence = 0; + for (const other of root.querySelectorAll ? root.querySelectorAll(el.tagName) : []) { + if (other === el) break; + if (normalizeText(other.textContent || '') === own) occurrence++; + } + let idx = -1; + for (let i = 0; i <= occurrence; i++) idx = all.indexOf(own, idx + 1); + if (idx < 0) return { prefix: '', suffix: '' }; + return { prefix: all.slice(Math.max(0, idx - CONTEXT), idx), suffix: all.slice(idx + own.length, idx + own.length + CONTEXT) }; +} + +export function describe(el, root) { + const text = normalizeText(el.textContent || ''); + const exact = text.slice(0, QUOTE_MAX); + const { prefix, suffix } = contextAround(el, root); + return { + css: cssPathFor(el), + path: structuralPathFor(el), + quote: { exact, prefix, suffix }, + tag: el.tagName.toLowerCase(), + label: normalizeText(el.getAttribute?.('aria-label') || el.getAttribute?.('alt') || el.getAttribute?.('title') || '').slice(0, 80) + }; +} + +// Dice coefficient on word bigrams; 1 is identical. +export function similarity(a, b) { + const w = (s) => normalizeText(s).toLowerCase().split(' ').filter(Boolean); + const A = w(a), B = w(b); + if (A.length === 0 && B.length === 0) return 1; + if (A.length < 2 || B.length < 2) return A.join(' ') === B.join(' ') ? 1 : 0; + const grams = (x) => { const m = new Map(); for (let i = 0; i < x.length - 1; i++) { const g = `${x[i]} ${x[i + 1]}`; m.set(g, (m.get(g) || 0) + 1); } return m; }; + const ga = grams(A), gb = grams(B); + let hit = 0; + for (const [g, n] of ga) hit += Math.min(n, gb.get(g) || 0); + return (2 * hit) / ((A.length - 1) + (B.length - 1)); +} + +function textOf(el) { + return normalizeText(el.textContent || '').slice(0, QUOTE_MAX); +} + +function verify(el, anchor) { + if (!el) return 0; + if (!anchor.quote?.exact) return 0.5; + return similarity(textOf(el), anchor.quote.exact); +} + +// Smallest element whose text contains the quote, preferring the same tag and +// the right context. +function findByQuote(anchor, root) { + const exact = anchor.quote?.exact; + if (!exact || !root.querySelectorAll) return null; + const all = normalizeText(root.textContent || ''); + if (!all.includes(exact)) return null; + let best = null; + let bestScore = -1; + for (const el of root.querySelectorAll('*')) { + if (SKIP_TAGS.has(el.tagName)) continue; + const t = normalizeText(el.textContent || ''); + if (!t.includes(exact)) continue; + // prefer tight matches: short text that still contains the quote + let score = exact.length / Math.max(t.length, 1); + if (el.tagName.toLowerCase() === anchor.tag) score += 0.05; + if (score > bestScore) { best = el; bestScore = score; } + } + return best; +} + +export function resolve(anchor, root) { + if (!anchor) return { el: null, how: 'orphan', confidence: 0 }; + const tryCss = (sel) => { try { return sel ? root.querySelector(sel) : null; } catch (e) { return null; } }; + let el = tryCss(anchor.css); + let conf = verify(el, anchor); + if (el && conf >= 0.6) return { el, how: 'css', confidence: conf }; + const byQuote = findByQuote(anchor, root); + if (byQuote) return { el: byQuote, how: 'quote', confidence: Math.max(0.6, similarity(textOf(byQuote), anchor.quote.exact)) }; + el = tryCss(anchor.path); + conf = verify(el, anchor); + if (el && conf >= 0.6) return { el, how: 'path', confidence: conf }; + if (el && !anchor.quote?.exact) return { el, how: 'path', confidence: 0.5 }; + return { el: null, how: 'orphan', confidence: 0 }; +} diff --git a/src/lib/explorer.js b/src/lib/explorer.js new file mode 100644 index 0000000..04b8e25 --- /dev/null +++ b/src/lib/explorer.js @@ -0,0 +1,227 @@ +// Shared saved-page browser. Selection stays in the view; only explicit Open +// actions create tabs. All note reads and mutations go through the worker. +// +// Navigation is a hierarchy, Sites > Pages > Page, and never a toggle. On a +// wide layout all three columns show; on a narrow one only the current level +// does, and up() climbs one level. The trail is a breadcrumb to any ancestor. +import { el, fmt, send } from './ui.js'; +import { cleanTitle } from './notes.js'; +import { createPagePicker } from './picker.js'; + +// A small mark with the number of linked pages; their names live in the tooltip. +function linkMark(row) { + const n = row.links || 0; + if (!n) return null; + const names = (row.linked || []).map((l) => cleanTitle(l.title) || l.title).join(', '); + return el('span', { class: 'explorer-link-count', title: names ? `Linked to ${names}` : `${n} linked page${n === 1 ? '' : 's'}`, 'aria-label': `${n} linked page${n === 1 ? '' : 's'}` }, [el('span', { class: 'mark', text: '⛓', 'aria-hidden': 'true' }), String(n)]); +} +const count = (n, word) => `${n} ${n === 1 ? word : ({ entry: 'entries', sticky: 'stickies' }[word] || `${word}s`)}`; +function summary(row) { + return [row.entries && count(row.entries, 'entry'), row.stickies && count(row.stickies, 'sticky'), row.captures && count(row.captures, 'capture')].filter(Boolean).join(' · ') || 'Empty page'; +} + +export function createExplorer(root, { search, includeCaptures, onCount = () => {}, onChange = () => {}, onNavigate = () => {} } = {}) { + root.classList.add('explorer'); + let rows = [], host = '', selected = null, revision = 0, detailRevision = 0; + let level = 0; // 0 sites, 1 pages of the chosen site, 2 the chosen page + let trailTitle = '', trailSite = ''; + function setLevel(next) { + level = Math.max(0, Math.min(2, next)); + if (level === 2 && !selected) level = 1; + root.dataset.level = String(level); + renderTrail(trailTitle); + const visible = rows.filter((r) => !host || r.host === host); + onNavigate(level, { host, title: trailTitle, site: trailSite, pages: visible.length, sites: new Set(rows.map((r) => r.host)).size }); + } + if (!search) { + search = el('input', { type: 'search', placeholder: 'Find a page or note…', 'aria-label': 'Search saved notes' }); + includeCaptures = el('input', { type: 'checkbox' }); + root.append(el('div', { class: 'explorer-search' }, [search, el('label', {}, [includeCaptures, ' Include capture-only pages'])])); + } + const message = el('div', { class: 'explorer-message', role: 'status', hidden: true }); + const siteList = el('div', { class: 'explorer-list', 'aria-label': 'Sites' }); + const pageList = el('div', { class: 'explorer-list', 'aria-label': 'Saved pages' }); + const siteLabel = el('summary', { text: 'Sites' }); + const pageLabel = el('summary', { text: 'Pages' }); + const sites = el('details', { class: 'explorer-column explorer-sites', open: true }, [siteLabel, siteList]); + const pages = el('details', { class: 'explorer-column explorer-pages', open: true }, [pageLabel, pageList]); + const preview = el('article', { class: 'explorer-preview', 'aria-label': 'Selected page', 'aria-live': 'polite' }); + const trail = el('nav', { class: 'explorer-trail', 'aria-label': 'Where you are' }); + root.append(message, trail, el('div', { class: 'explorer-columns' }, [sites, pages, preview])); + root.dataset.level = '0'; + // the columns are always open; the narrow layout shows one level at a time + for (const label of [siteLabel, pageLabel]) label.addEventListener('click', (e) => e.preventDefault()); + + function renderTrail(title) { + const crumbs = [el('button', { class: 'crumb', text: 'All sites', 'aria-current': level === 0 ? 'page' : null, onclick: () => setLevel(0) })]; + if (host || level >= 1) crumbs.push(el('button', { class: 'crumb', text: host || 'All pages', 'aria-current': level === 1 ? 'page' : null, onclick: () => setLevel(1) })); + if (title && level === 2) crumbs.push(el('span', { class: 'crumb current', text: title, 'aria-current': 'page' })); + trail.replaceChildren(); + crumbs.forEach((c, i) => { if (i) trail.append(el('span', { class: 'sep', text: '›', 'aria-hidden': 'true' })); trail.append(c); }); + } + + // Arrow keys move through the buttons in either navigation column. + for (const list of [siteList, pageList]) list.addEventListener('keydown', (e) => { + if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) return; + const buttons = [...list.querySelectorAll('button')]; + const index = buttons.indexOf(document.activeElement); + if (index < 0) return; + e.preventDefault(); + const next = e.key === 'Home' ? 0 : e.key === 'End' ? buttons.length - 1 : Math.max(0, Math.min(buttons.length - 1, index + (e.key === 'ArrowDown' ? 1 : -1))); + buttons[next]?.focus(); + }); + + function error(e) { message.hidden = false; message.textContent = e.message; } + function action(text, fn, className = '') { + return el('button', { text, class: className, onclick: async (e) => { + e.currentTarget.disabled = true; + try { await fn(); } catch (err) { error(err); } + finally { e.target.disabled = false; } + } }); + } + function renderSites() { + const hosts = new Map(); + for (const row of rows) hosts.set(row.host, (hosts.get(row.host) || 0) + 1); + if (host && !hosts.has(host)) host = ''; + siteList.replaceChildren(); + for (const [name, total] of [['', rows.length], ...[...hosts].sort(([a], [b]) => a.localeCompare(b))]) { + siteList.append(el('button', { class: `explorer-site${host === name ? ' selected' : ''}`, 'aria-pressed': String(host === name), 'data-host': name, onclick: () => { + host = name; + renderSites(); renderPages(); + setLevel(1); + } }, [el('span', { class: 'explorer-folder', text: name ? '▱' : '▦', 'aria-hidden': 'true' }), el('span', { class: 'explorer-label', text: name || 'All sites' }), el('span', { class: 'explorer-count', text: String(total) })])); + } + siteLabel.textContent = host || 'Sites'; + } + function renderPages() { + const visible = rows.filter((r) => !host || r.host === host); + if (!visible.some((r) => r.id === selected)) selected = visible[0]?.id || null; + pageLabel.textContent = `${host || 'All pages'} · ${visible.length}`; + pageList.replaceChildren(); + // across all sites, pages are listed by site + const grouped = host ? [['', visible]] : [...visible.reduce((m, r) => m.set(r.host, [...(m.get(r.host) || []), r]), new Map())].sort(([a], [b]) => a.localeCompare(b)); + for (const [site, group] of grouped) { + if (site) pageList.append(el('div', { class: 'explorer-group', role: 'heading', 'aria-level': '3' }, [el('span', { class: 'explorer-folder', text: '▱', 'aria-hidden': 'true' }), el('span', { class: 'explorer-label', text: site }), el('span', { class: 'explorer-count', text: String(group.length) })])); + for (const row of group) { + const button = el('button', { class: `explorer-page${row.id === selected ? ' selected' : ''}`, 'data-id': row.id, 'aria-pressed': String(row.id === selected), title: cleanTitle(row.title), onclick: () => { + selected = row.id; + for (const b of pageList.querySelectorAll('button')) { b.classList.toggle('selected', b.dataset.id === selected); b.setAttribute('aria-pressed', String(b.dataset.id === selected)); } + showDetail(row.id); + setLevel(2); + } }, [ + el('span', { class: 'explorer-page-title', text: cleanTitle(row.title) || row.url }), + el('span', { class: 'explorer-page-meta' }, [summary(row), linkMark(row)]), + el('span', { class: 'explorer-page-excerpt', text: row.preview || row.host }) + ]); + pageList.append(button); + } + } + if (selected) showDetail(selected); + else { + ++detailRevision; + preview.replaceChildren(el('div', { class: 'explorer-empty' }, [el('b', { text: search.value ? 'No matching notes' : 'Your notebook starts here' }), el('p', { text: search.value ? 'Try a page title, a quote or a word from your notes.' : 'Open a page and click the toolbar icon to write your first note.' })])); + trailTitle = ''; trailSite = ''; + if (level === 2) setLevel(1); else renderTrail(); + } + } + async function showDetail(id) { + const version = ++detailRevision; + preview.replaceChildren(el('p', { class: 'muted', text: 'Loading notes…' })); + try { + const note = await send('get', { id }); + if (version !== detailRevision) return; + if (!note) { await reload(); return; } + trailTitle = cleanTitle(note.title); + trailSite = note.host || ''; + // the page arrived after the level did: tell the host again + if (level === 2) setLevel(2); else renderTrail(trailTitle); + const actions = el('div', { class: 'explorer-actions' }, [ + action('Open page ↗', () => send('openNote', { id, page: true }), 'primary'), + action('Full details', () => send('openNote', { id })) + ]); + preview.replaceChildren(el('div', { class: 'explorer-preview-head' }, [el('span', { class: 'eyebrow', text: 'Saved page' }), el('h3', { text: cleanTitle(note.title) }), el('div', { class: 'explorer-url', text: note.url }), actions])); + const addSection = (title, items, type, content) => { + if (!items.length) return; + preview.append(el('h4', { text: `${title} · ${items.length}` })); + items.forEach((item, i) => { + const { label, excerpt, body } = content(item, i); + preview.append(el('details', { class: `explorer-item ${type}`, open: i === 0 }, [el('summary', {}, [el('span', { class: 'explorer-item-label', text: label }), el('span', { class: 'explorer-item-excerpt', text: excerpt })]), body])); + }); + }; + // linked pages: click one to move to it here; + Link opens the picker + const linked = await send('links', { id }); + if (version !== detailRevision) return; + const linkBox = el('div', { class: 'explorer-links' }); + const linkHead = el('h4', { text: `Linked pages · ${linked.length}` }); + const linkList = el('ul', { class: 'explorer-link-list' }); + for (const row of linked) { + linkList.append(el('li', { class: row.missing ? 'missing' : '' }, [ + el('button', { class: 'explorer-link', disabled: row.missing || undefined, title: row.missing ? 'That page\'s note is gone' : 'Show this page', onclick: () => show(row.id) }, [el('span', { class: 'host', text: row.host || '·' }), el('span', { class: 'title', text: cleanTitle(row.title) || row.url || row.key })]), + action('Unlink', async () => { await send('unlink', { id, otherId: row.id }); await showDetail(id); }, 'ghost tiny') + ])); + } + const pickerHolder = el('div', { class: 'explorer-picker', hidden: true }); + let picker = null; + const closePicker = () => { picker = null; pickerHolder.hidden = true; pickerHolder.replaceChildren(); addLink.setAttribute('aria-expanded', 'false'); }; + const addLink = el('button', { class: 'tiny', text: '+ Link a page', 'aria-expanded': 'false', title: 'Connect this page to another saved page, on any site', onclick: () => { + if (picker) { closePicker(); return; } + picker = createPagePicker({ exclude: new Set([id, ...linked.map((r) => r.id)]), placeholder: 'Type to find a page to link…', onPick: async (row) => { + closePicker(); + try { await send('link', { id, otherId: row.id }); await showDetail(id); onChange(); } catch (e) { error(e); } + }, onClose: closePicker }); + pickerHolder.replaceChildren(picker.root); pickerHolder.hidden = false; addLink.setAttribute('aria-expanded', 'true'); picker.focus(); + } }); + linkHead.append(' ', addLink); + linkBox.append(linkHead, pickerHolder, linkList); + if (!linked.length) linkList.append(el('li', { class: 'muted', text: 'Nothing linked yet. Connect this page to another saved page, on any site.' })); + preview.append(linkBox); + addSection('Notes', [...note.entries].reverse(), 'note-entry', (e) => ({ label: fmt(e.at), excerpt: e.text, body: el('div', { class: 'explorer-body', text: e.text }) })); + addSection('On the page', note.stickies || [], 'sticky', (s, i) => ({ label: `Sticky ${i + 1}${s.found === false ? ' · location missing' : ''}`, excerpt: s.text || 'No comment yet', body: el('div', { class: 'explorer-body' }, [el('blockquote', { text: s.anchor?.quote?.exact || s.anchor?.label || 'Page element' }), el('p', { text: s.text || 'No comment yet' })]) })); + if (note.stickies?.length) preview.append(action('Copy feedback', async () => navigator.clipboard.writeText(await send('feedback', { id })))); + addSection('Captures', [...(note.snapshots || [])].reverse(), 'capture', (c) => ({ label: fmt(c.at), excerpt: cleanTitle(c.title) || 'Page capture', body: el('div', { class: 'explorer-body', text: c.text || 'No page text.' }) })); + if (!note.entries.length && !note.stickies?.length) preview.append(el('p', { class: 'muted', text: 'A saved capture. Open the page to add your own notes.' })); + preview.append(el('div', { class: 'explorer-bottom' }, [el('span', { text: `Updated ${fmt(note.updated)}` }), action('Delete page notes', async () => { + if (!confirm(`Delete all notes for “${cleanTitle(note.title)}”?`)) return; + await send('deleteNote', { id }); await reload(); onChange(); + }, 'ghost')])); + } catch (e) { if (version === detailRevision) { preview.replaceChildren(); error(e); } } + } + async function reload() { + const version = ++revision; + ++detailRevision; + try { + const result = await send('list', { query: search.value, includeCaptures: includeCaptures.checked }); + if (version !== revision) return; + rows = result; message.hidden = true; + onCount(rows.length); renderSites(); renderPages(); + } catch (e) { if (version === revision) error(e); } + } + // Climb one level; false at the top so the host can hide its back control. + function up() { + if (level === 0) return false; + setLevel(level - 1); + return true; + } + // Land on a site's page list, with one of its pages highlighted: where the + // sidebar's back arrow arrives from the page it was showing. + function focus({ host: wantHost = '', id = null } = {}) { + const known = rows.some((r) => r.host === wantHost); + host = known ? wantHost : ''; + if (id && rows.some((r) => r.id === id)) selected = id; + renderSites(); renderPages(); + setLevel(known ? 1 : 0); + } + // Jump to a page by id: its site becomes the filter if the current one hides it. + function show(id) { + const row = rows.find((r) => r.id === id); + if (!row) return false; + if (host && row.host !== host) host = row.host; + selected = id; + renderSites(); renderPages(); + setLevel(2); + return true; + } + search.addEventListener('input', () => { reload().then(() => { if (search.value && level === 0) setLevel(1); }); }); + includeCaptures.addEventListener('change', reload); + return { reload, up, focus, show, depth: () => level }; +} diff --git a/src/lib/notes.js b/src/lib/notes.js new file mode 100644 index 0000000..c830fbb --- /dev/null +++ b/src/lib/notes.js @@ -0,0 +1,423 @@ +// Pure note model: one note per canonical page, dated entries inside it, +// Markdown with YAML frontmatter as the interchange format (Obsidian-friendly). +// No Chrome APIs here. + +import { canonicalize, hostOf } from './url.js'; + +export const NOTE_VERSION = 1; + +// Stable id for a canonical key: short hash, safe for storage keys and filenames. +export function noteId(key) { + let h1 = 0x811c9dc5; + let h2 = 0x01000193; + for (let i = 0; i < key.length; i++) { + const c = key.charCodeAt(i); + h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0; + h2 = Math.imul(h2 ^ c, 0x811c9dc5) >>> 0; + } + return (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')).slice(0, 12); +} + +export function identify(url, compiledRules, normalization) { + const { key, rule } = canonicalize(url, compiledRules, normalization); + return { key, rule, id: noteId(key) }; +} + +// Page titles carry one thing that is not the page: an unread counter at the +// front, "(3) Jane Doe | LinkedIn" (LinkedIn, Gmail, Slack, X). The site name +// at the end is part of what the page is, so it stays. +export function cleanTitle(title) { + const t = (title || '').replace(/\s+/g, ' ').trim(); + const cleaned = t.replace(/^(?:[\(\[]\d+\+?[\)\]]\s*|[•·]\s*)+/, ''); + return cleaned || t; +} + +// Upgrade the display title only. Captures remain a record of what was seen, +// and cleanup must not move an old note to the top of the recent list. +export function sanitizeSavedTitle(note) { + if (!/(^|\.)linkedin\.com$/i.test(hostOf(note.url) || '')) return note; + const title = cleanTitle(note.title); + return title === note.title ? note : { ...note, title }; +} + +export function newNote({ url, title, key, rule, now = new Date() }) { + const iso = now.toISOString(); + return { + version: NOTE_VERSION, + id: noteId(key), + key, + rule: rule || null, + url, + title: cleanTitle(title) || url, + host: hostOf(url) || '', + tags: [], + created: iso, + updated: iso, + remind: null, + entries: [], + snapshot: null, + snapshots: [], + stickies: [], + links: [] + }; +} + +// ----- links: pages the user connected, across sites -------------------- +// A link is symmetric and stored on both notes as the other page's canonical +// key, so it survives export (keys are readable) and resolves to an id with +// noteId(). Linking is the user's judgement; nothing is inferred. + +export function linkNotes(a, b, now = new Date()) { + if (!a || !b || a.id === b.id) throw new Error('A page cannot link to itself.'); + const iso = now.toISOString(); + const add = (note, key) => (note.links || []).includes(key) ? note : { ...note, links: [...(note.links || []), key], updated: iso }; + return [add(a, b.key), add(b, a.key)]; +} + +export function unlinkNotes(a, b, now = new Date()) { + const iso = now.toISOString(); + const drop = (note, key) => (note.links || []).includes(key) ? { ...note, links: note.links.filter((k) => k !== key), updated: iso } : note; + return [drop(a, b.key), drop(b, a.key)]; +} + +// ----- stickies: notes attached to an element on the page -------------------- + +export function stickyId() { + return Math.random().toString(36).slice(2, 10); +} + +export function addSticky(note, { anchor, text = '' }, now = new Date()) { + const iso = now.toISOString(); + const sticky = { id: stickyId(), anchor, text: (text || '').trim(), created: iso, updated: iso }; + return { ...note, updated: iso, stickies: [...(note.stickies || []), sticky] }; +} + +export function updateSticky(note, id, patch, now = new Date()) { + const iso = now.toISOString(); + let changed = false; + const stickies = (note.stickies || []).map((s) => { + if (s.id !== id) return s; + const next = { ...s, ...patch, text: patch.text !== undefined ? (patch.text || '').trim() : s.text, updated: iso }; + changed = JSON.stringify(next) !== JSON.stringify({ ...s, updated: iso }); + return next; + }); + return changed ? { ...note, updated: iso, stickies } : note; +} + +export function removeSticky(note, id, now = new Date()) { + const stickies = (note.stickies || []).filter((s) => s.id !== id); + if (stickies.length === (note.stickies || []).length) return note; + return { ...note, updated: now.toISOString(), stickies }; +} + +// The feedback a reviewer sends: a numbered list with the quoted context. +export function feedbackMarkdown(note) { + const lines = [`Feedback on ${note.title}`, note.url, '']; + (note.stickies || []).forEach((s, i) => { + const quote = s.anchor?.quote?.exact ? `"${s.anchor.quote.exact.slice(0, 120)}${s.anchor.quote.exact.length > 120 ? '…' : ''}"` : (s.anchor?.label || s.anchor?.tag || 'element'); + lines.push(`${i + 1}. ${quote}`); + if (s.text) lines.push(` ${s.text.replace(/\n/g, '\n ')}`); + }); + return lines.join('\n'); +} + +export function addEntry(note, text, now = new Date()) { + const body = (text || '').trim(); + if (!body) return note; + const iso = now.toISOString(); + return { ...note, updated: iso, entries: [...note.entries, { at: iso, text: body }] }; +} + +// Local calendar day of a timestamp, for "today's entry". +export function localDay(iso, now = new Date()) { + const d = iso ? new Date(iso) : now; + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} + +export function todayIndex(note, now = new Date()) { + const today = localDay(null, now); + const entries = note?.entries || []; + for (let i = entries.length - 1; i >= 0; i--) { + if (localDay(entries[i].at, now) === today) return i; + } + return -1; +} + +// The editor writes today's entry as the user types: create it on the first +// keystroke, update it after, remove it when emptied. The entry keeps its +// first timestamp so a day's note is one dated block. +export function setToday(note, text, now = new Date()) { + const body = (text || '').trim(); + const i = todayIndex(note, now); + if (i < 0) return body ? addEntry(note, body, now) : note; + if (!body) return removeEntry(note, i, now); + if (note.entries[i].text === body) return note; + return { ...note, updated: now.toISOString(), entries: note.entries.map((e, j) => (j === i ? { ...e, text: body, edited: now.toISOString() } : e)) }; +} + +export function updateEntry(note, index, text, now = new Date()) { + const entries = note.entries.map((e, i) => (i === index ? { ...e, text: (text || '').trim(), edited: now.toISOString() } : e)); + return { ...note, updated: now.toISOString(), entries: entries.filter((e) => e.text) }; +} + +export function removeEntry(note, index, now = new Date()) { + return { ...note, updated: now.toISOString(), entries: note.entries.filter((_, i) => i !== index) }; +} + +export function setReminder(note, when, now = new Date()) { + return { ...note, updated: now.toISOString(), remind: when ? new Date(when).toISOString() : null }; +} + +export const SNAPSHOT_LIMIT = 12; + +function textHash(s) { + let h = 0x811c9dc5; + for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193) >>> 0; + return h.toString(16); +} + +// Snapshots are what the page looked like when you were there. The newest is +// also mirrored in `snapshot` for older readers. Identical captures (same text) +// only bump the newest snapshot's `seen` time, so history stays meaningful. +export function addSnapshot(note, capture, now = new Date()) { + if (!capture || !capture.text) return note; + const iso = now.toISOString(); + const hash = textHash(`${capture.title || ''}\n${capture.text}`); + const history = [...(note.snapshots || [])]; + const last = history[history.length - 1]; + if (last && last.hash === hash) { + // same page as last time: note that it was seen again, but do not + // rewrite the note for a repeat within the hour + if (now - new Date(last.seen || last.at) < 3600000) return note; + history[history.length - 1] = { ...last, seen: iso }; + } else { + history.push({ at: iso, seen: iso, hash, title: capture.title || '', description: capture.description || '', text: capture.text, meta: capture.meta || null, structured: capture.structured || null }); + while (history.length > SNAPSHOT_LIMIT) history.splice(1, 1); // keep the first ever and the recent ones + } + const newest = history[history.length - 1]; + const title = cleanTitle(newest.title) || note.title; + return { ...note, title, snapshots: history, snapshot: { at: newest.at, text: newest.text, title: newest.title, description: newest.description } }; +} + +export function setSnapshot(note, snapshot, now = new Date()) { + return addSnapshot(note, snapshot, now); +} + +// ----- Markdown ------------------------------------------------------------ + +function yamlString(v) { + const s = String(v ?? ''); + return /^[\w./:+-]*$/.test(s) && s !== '' ? s : JSON.stringify(s); +} + +function fmtStamp(iso) { + // local wall-clock stamp for headings: YYYY-MM-DD HH:MM + const d = new Date(iso); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} + +export function slugify(s) { + return (s || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 60) || 'note'; +} + +export function fileNameFor(note) { + return `${slugify(note.title)}-${note.id}.md`; +} + +export function toMarkdown(note) { + const lines = ['---']; + lines.push(`title: ${yamlString(note.title)}`); + lines.push(`url: ${yamlString(note.url)}`); + lines.push(`key: ${yamlString(note.key)}`); + if (note.rule) lines.push(`rule: ${yamlString(note.rule)}`); + lines.push(`created: ${note.created}`); + lines.push(`updated: ${note.updated}`); + if (note.tags.length) lines.push(`tags: [${note.tags.map(yamlString).join(', ')}]`); + if (note.remind) lines.push(`remind: ${note.remind}`); + if (note.links?.length) lines.push(`links: [${note.links.map(yamlString).join(', ')}]`); + if (note.snapshot?.text) lines.push(`excerpt: ${JSON.stringify(note.snapshot.text.slice(0, 280))}`); + lines.push('app: marginalia'); + lines.push('---', ''); + for (const e of [...note.entries].reverse()) { + lines.push(`## ${fmtStamp(e.at)}`, '', e.text, ''); + } + if (note.stickies?.length) { + lines.push('## Stickies', ''); + note.stickies.forEach((s, i) => { + const quote = s.anchor?.quote?.exact ? `"${s.anchor.quote.exact}"` : (s.anchor?.label || s.anchor?.tag || 'element'); + lines.push(`${i + 1}. **${quote}** ${s.text ? `— ${s.text.replace(/\n/g, ' ')}` : ''}`.trimEnd()); + lines.push(` `); + }); + lines.push(''); + } + return lines.join('\n'); +} + +function parseYamlValue(raw) { + const v = raw.trim(); + if (v.startsWith('"')) { + try { return JSON.parse(v); } catch (e) { return v.slice(1, -1); } + } + if (v.startsWith('[') && v.endsWith(']')) { + return v.slice(1, -1).split(',').map((x) => parseYamlValue(x)).filter((x) => x !== ''); + } + return v; +} + +// Parses a Markdown note. Headings "## YYYY-MM-DD HH:MM" start dated entries; +// text before the first heading becomes an undated entry stamped at `created`. +export function fromMarkdown(text) { + const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text.replace(/\r\n/g, '\n')); + if (!m) throw new Error('Missing frontmatter'); + const meta = {}; + for (const line of m[1].split('\n')) { + const i = line.indexOf(':'); + if (i > 0) meta[line.slice(0, i).trim()] = parseYamlValue(line.slice(i + 1)); + } + if (!meta.url || !meta.key) throw new Error('Frontmatter needs url and key'); + let body = m[2]; + // stickies live in their own section, carried with hidden anchors + const stickies = []; + const sm = /\n## Stickies\n([\s\S]*)$/.exec(`\n${body}`); + if (sm) { + body = body.slice(0, body.length - sm[0].length + 1); + const items = sm[1].split(/\n(?=\d+\. )/); + for (const item of items) { + const head = /^\d+\. \*\*(.*?)\*\*\s*(?:— (.*))?$/m.exec(item); + const meta = //.exec(item); + if (!head) continue; + let carried = {}; + try { carried = meta ? JSON.parse(meta[1]) : {}; } catch (e) { carried = {}; } + const quoteText = head[1].replace(/^"|"$/g, ''); + stickies.push({ + id: carried.id || stickyId(), + anchor: carried.anchor || { quote: { exact: quoteText, prefix: '', suffix: '' } }, + text: (head[2] || '').trim(), + created: carried.created || meta?.created || new Date().toISOString(), + updated: carried.updated || new Date().toISOString() + }); + } + } + const entries = []; + const re = /^## (\d{4}-\d{2}-\d{2})(?: (\d{2}:\d{2}))?\s*$/gm; + let last = null; + let match; + const pushEntry = (at, chunk) => { + const t = chunk.trim(); + if (t) entries.push({ at, text: t }); + }; + const first = re.exec(body); + const preamble = body.slice(0, (first || { index: body.length }).index); + re.lastIndex = 0; + // an undated preamble belongs before the first dated section, not "now" + const preambleAt = meta.created || (first ? new Date(`${first[1]}T${first[2] || '00:00'}:00`).toISOString() : new Date().toISOString()); + pushEntry(preambleAt, preamble); + while ((match = re.exec(body))) { + if (last) pushEntry(last.at, body.slice(last.end, match.index)); + const at = new Date(`${match[1]}T${match[2] || '00:00'}:00`).toISOString(); + last = { at, end: match.index + match[0].length }; + } + if (last) pushEntry(last.at, body.slice(last.end)); + // stable sort: equal timestamps keep file order + entries.forEach((e, i) => { e._i = i; }); + entries.sort((a, b) => a.at.localeCompare(b.at) || a._i - b._i); + entries.forEach((e) => { delete e._i; }); + return { + version: NOTE_VERSION, + id: noteId(meta.key), + key: meta.key, + rule: meta.rule || null, + url: meta.url, + title: meta.title || meta.url, + host: hostOf(meta.url) || '', + tags: Array.isArray(meta.tags) ? meta.tags : [], + created: meta.created || entries[0]?.at || new Date().toISOString(), + updated: meta.updated || entries[entries.length - 1]?.at || new Date().toISOString(), + remind: meta.remind || null, + entries, + snapshot: meta.excerpt ? { at: meta.updated || null, text: meta.excerpt } : null, + snapshots: [], + stickies, + links: Array.isArray(meta.links) ? meta.links.map(String) : [] + }; +} + +// Markdown headings carry minutes, not seconds, so entries are matched to the +// minute plus text when merging. +const entryKey = (e) => `${new Date(e.at).toISOString().slice(0, 16)}|${e.text}`; + +// Merge an imported note into an existing one: union of entries by +// minute-stamp and text. An existing note keeps its own reminder state; a +// reminder from the file is only honoured for a new note and only if it is +// still in the future. +export function mergeNotes(existing, incoming, now = new Date()) { + const futureRemind = incoming.remind && new Date(incoming.remind) > now ? incoming.remind : null; + if (!existing) return { ...incoming, remind: futureRemind }; + const seen = new Set(existing.entries.map(entryKey)); + const entries = [...existing.entries]; + for (const e of incoming.entries) { + if (!seen.has(entryKey(e))) entries.push(e); + } + entries.sort((a, b) => a.at.localeCompare(b.at)); + return { + ...existing, + title: incoming.updated > existing.updated ? incoming.title : existing.title, + tags: [...new Set([...existing.tags, ...incoming.tags])], + updated: [existing.updated, incoming.updated].sort().pop(), + remind: existing.remind, + snapshot: existing.snapshot || incoming.snapshot, + snapshots: existing.snapshots?.length ? existing.snapshots : incoming.snapshots || [], + stickies: [...(existing.stickies || []), ...(incoming.stickies || []).filter((s) => !(existing.stickies || []).some((e) => e.id === s.id))], + links: [...new Set([...(existing.links || []), ...(incoming.links || [])])], + entries + }; +} + +// ----- search / summaries -------------------------------------------------- + +export function indexEntry(note) { + const last = note.entries[note.entries.length - 1]; + return { + id: note.id, + key: note.key, + url: note.url, + title: note.title, + host: note.host, + updated: note.updated, + remind: note.remind, + entries: note.entries.length, + captures: (note.snapshots || []).length, + stickies: (note.stickies || []).length, + links: (note.links || []).length, + linkKeys: note.links || [], + rule: note.rule || null, + preview: (last?.text || '').slice(0, 120) + }; +} + +export function matchesQuery(note, query) { + const q = (query || '').trim().toLowerCase(); + if (!q) return true; + const hay = [note.title, note.url, note.host, note.tags.join(' '), ...note.entries.map((e) => e.text), ...(note.stickies || []).flatMap((s) => [s.text, s.anchor?.quote?.exact || ''])].join('\n').toLowerCase(); + return q.split(/\s+/).every((term) => hay.includes(term)); +} + +// Snooze presets, relative to `now`: returns an ISO timestamp. +export function presetTime(preset, now = new Date()) { + const d = new Date(now); + switch (preset) { + case '1h': d.setHours(d.getHours() + 1); break; + case '3h': d.setHours(d.getHours() + 3); break; + case 'tonight': d.setHours(18, 0, 0, 0); if (d <= now) d.setDate(d.getDate() + 1); break; + case 'tomorrow': d.setDate(d.getDate() + 1); d.setHours(9, 0, 0, 0); break; + case 'nextweek': d.setDate(d.getDate() + ((8 - d.getDay()) % 7 || 7)); d.setHours(9, 0, 0, 0); break; + default: return null; + } + return d.toISOString(); +} diff --git a/src/lib/picker.js b/src/lib/picker.js new file mode 100644 index 0000000..505d7f5 --- /dev/null +++ b/src/lib/picker.js @@ -0,0 +1,59 @@ +// Type-to-find picker over the saved pages. Loads the index once per open, +// filters as you type (every word must match the title, site, URL or the +// latest note), and is a keyboard-first combobox: arrows move, Enter picks, +// Esc closes. Used to link pages from the popup, the sidebar and All notes. +import { el, send } from './ui.js'; +import { cleanTitle } from './notes.js'; + +export function matchRows(rows, query, { exclude = new Set() } = {}) { + const words = query.toLowerCase().split(/\s+/).filter(Boolean); + return rows + .filter((r) => !exclude.has(r.id)) + .filter((r) => { + const hay = `${cleanTitle(r.title)} ${r.host} ${r.url} ${r.preview || ''}`.toLowerCase(); + return words.every((w) => hay.includes(w)); + }) + .sort((a, b) => { + // a title hit before a hit anywhere else, then most recent first + const t = (r) => words.length && words.every((w) => cleanTitle(r.title).toLowerCase().includes(w)) ? 0 : 1; + return t(a) - t(b) || b.updated.localeCompare(a.updated); + }); +} + +export function createPagePicker({ placeholder = 'Type to find a page…', exclude = new Set(), limit = 8, onPick, onClose = () => {} }) { + const listId = `picker-${Math.random().toString(36).slice(2, 8)}`; + const input = el('input', { type: 'search', placeholder, role: 'combobox', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'aria-controls': listId, autocomplete: 'off', spellcheck: 'false' }); + const list = el('ul', { id: listId, class: 'picker-list', role: 'listbox' }); + const status = el('div', { class: 'picker-status muted', role: 'status' }); + const root = el('div', { class: 'picker' }, [input, list, status]); + let rows = null, shown = [], active = -1, closed = false; + + function render() { + const query = input.value.trim(); + shown = rows ? matchRows(rows, query, { exclude }).slice(0, limit) : []; + list.replaceChildren(); + shown.forEach((r, i) => { + list.append(el('li', { id: `${listId}-${i}`, role: 'option', 'aria-selected': String(i === active), class: i === active ? 'active' : '', onmousedown: (e) => { e.preventDefault(); pick(r); }, onmousemove: () => { if (active !== i) { active = i; render(); } } }, [ + el('span', { class: 'picker-title', text: cleanTitle(r.title) || r.url }), + el('span', { class: 'picker-meta', text: [r.host, r.entries ? `${r.entries} ${r.entries === 1 ? 'entry' : 'entries'}` : '', r.stickies ? `${r.stickies} ${r.stickies === 1 ? 'sticky' : 'stickies'}` : ''].filter(Boolean).join(' · ') }) + ])); + }); + input.setAttribute('aria-expanded', String(shown.length > 0)); + input.setAttribute('aria-activedescendant', active >= 0 && shown[active] ? `${listId}-${active}` : ''); + const total = rows ? matchRows(rows, query, { exclude }).length : 0; + status.textContent = !rows ? 'Loading your pages…' : total === 0 ? (query ? 'No saved page matches.' : 'No other saved pages yet.') : total > shown.length ? `${shown.length} of ${total} pages, keep typing to narrow` : `${total} page${total === 1 ? '' : 's'}`; + } + function pick(row) { if (closed) return; closed = true; onPick(row); } + function close() { if (closed) return; closed = true; onClose(); } + input.addEventListener('input', () => { active = shown.length ? 0 : -1; render(); if (active === 0) render(); }); + input.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(shown.length - 1, active + 1); render(); } + else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(0, active - 1); render(); } + else if (e.key === 'Enter') { e.preventDefault(); if (shown[active]) pick(shown[active]); else if (shown.length === 1) pick(shown[0]); } + else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); } + }); + input.addEventListener('blur', () => setTimeout(() => { if (!root.contains(document.activeElement)) close(); }, 120)); + render(); + send('list', {}).then((r) => { rows = r; active = r.length && matchRows(rows, '', { exclude }).length ? 0 : -1; render(); }).catch((e) => { rows = []; render(); status.textContent = e.message; }); + return { root, focus: () => input.focus(), close }; +} diff --git a/src/lib/profile.js b/src/lib/profile.js new file mode 100644 index 0000000..910b936 --- /dev/null +++ b/src/lib/profile.js @@ -0,0 +1,359 @@ +// Pure profile intelligence: parse a captured page's text into a structured +// work history, then analyse it for things a careful reader would ask about. +// Nothing here is a verdict. Findings are phrased as questions and labelled +// "info" or "ask", never as conclusions about a person. No Chrome APIs. + +const MONTHS = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, sept: 8, oct: 9, nov: 10, dec: 11 }; +const MONTH_RE = '(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\\.?'; +// "Jan 2020 - Present · 4 yrs 9 mos", "2019 - 2021", "Mar 2018 – Dec 2019" +const RANGE_RE = new RegExp(`^(?:(${MONTH_RE})\\s+)?(\\d{4})\\s*[-–—]\\s*(?:(present|current|now)|(?:(${MONTH_RE})\\s+)?(\\d{4}))(?:\\s*[·•]\\s*(.+))?$`, 'i'); +const DURATION_RE = /^(?:(\d+)\s*yrs?)?\s*(?:(\d+)\s*mos?)?$/i; +const EMPLOYMENT_TYPES = ['full-time', 'part-time', 'self-employed', 'freelance', 'contract', 'internship', 'apprenticeship', 'seasonal', 'temporary']; +const SECTION_HEADINGS = ['about', 'activity', 'experience', 'education', 'licenses & certifications', 'licenses and certifications', 'projects', 'skills', 'courses', 'honors & awards', 'honors and awards', 'languages', 'volunteering', 'publications', 'recommendations', 'interests', 'organizations', 'patents', 'test scores', 'causes']; +const NOISE_RE = /^(show all \d+ .*|…see more|see more|…|·|\d+ (followers|connections)|\d+ mutual connections?|helped me get this job|skills:.*|\d+ endorsements?)$/i; + +export function parseMonth(s) { + if (!s) return null; + const m = MONTHS[s.toLowerCase().replace('.', '').slice(0, 4)] ?? MONTHS[s.toLowerCase().replace('.', '').slice(0, 3)]; + return m === undefined ? null : m; +} + +// Returns { start: {y, m|null}, end: {y, m|null} | null (present), note } +export function parseDateRange(line) { + const m = RANGE_RE.exec(line.trim()); + if (!m) return null; + const start = { y: Number(m[2]), m: parseMonth(m[1]) }; + const end = m[3] ? null : { y: Number(m[5]), m: parseMonth(m[4]) }; + return { start, end, note: (m[6] || '').trim() }; +} + +export function parseDuration(s) { + const m = DURATION_RE.exec((s || '').trim()); + if (!m || (!m[1] && !m[2])) return null; + return (Number(m[1] || 0) * 12) + Number(m[2] || 0); +} + +// Month index for comparisons; unknown months are treated as January for a +// start and December for an end, which is the generous reading. +export function monthIndex(d, { asEnd = false } = {}) { + if (!d) return null; + return d.y * 12 + (d.m ?? (asEnd ? 11 : 0)); +} + +export function fmtDate(d) { + if (!d) return 'Present'; + const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return d.m === null || d.m === undefined ? String(d.y) : `${names[d.m]} ${d.y}`; +} + +function cleanLines(text) { + const out = []; + for (const raw of text.replace(/\r/g, '').split('\n')) { + const l = raw.replace(/\s+/g, ' ').trim(); + if (!l || NOISE_RE.test(l)) continue; + // LinkedIn renders most text twice (a visible span and a visually + // hidden copy), which innerText reports as consecutive duplicates + if (out.length && out[out.length - 1] === l) continue; + out.push(l); + } + return out; +} + +// Slices a named section out of the line list: from the heading line to the +// next known heading. +export function sectionLines(lines, name) { + const lower = lines.map((l) => l.toLowerCase()); + const start = lower.indexOf(name.toLowerCase()); + if (start < 0) return []; + let end = lines.length; + for (let i = start + 1; i < lines.length; i++) { + if (SECTION_HEADINGS.includes(lower[i]) && lower[i] !== name.toLowerCase()) { end = i; break; } + } + return lines.slice(start + 1, end); +} + +function splitCompanyLine(line) { + // "Acme Corp · Full-time" or "Acme Corp" or "Full-time · 5 yrs 2 mos" + const parts = line.split(/\s*[·•]\s*/); + const type = parts.find((p) => EMPLOYMENT_TYPES.includes(p.toLowerCase())) || null; + const rest = parts.filter((p) => p !== type); + return { type, rest }; +} + +// Parses the Experience section. Handles both flat positions +// Title / Company · Type / Range · Duration / Location +// and grouped positions under one employer +// Company / Type · Duration / Title / Range / Location / Title / Range ... +export function parseExperience(lines) { + const positions = []; + let i = 0; + while (i < lines.length) { + const a = lines[i]; + const b = lines[i + 1] || ''; + const c = lines[i + 2] || ''; + const rangeAtC = parseDateRange(c); + const rangeAtB = parseDateRange(b); + if (rangeAtC && !parseDateRange(a) && !rangeAtB) { + // flat: a=title, b=company·type, c=range + const { type, rest } = splitCompanyLine(b); + const pos = { title: a, company: rest[0] || '', type, ...rangeAtC, duration: parseDuration(rangeAtC.note), location: '' }; + i += 3; + if (lines[i] && !parseDateRange(lines[i]) && !parseDateRange(lines[i + 1] || '') && /,|remote|hybrid|on-site/i.test(lines[i]) && !parseDateRange(lines[i + 2] || '')) { + pos.location = lines[i]; + i += 1; + } + positions.push(pos); + continue; + } + const bParts = splitCompanyLine(b); + const groupedHeader = !parseDateRange(a) && !rangeAtB && bParts.type && bParts.rest.every((p) => parseDuration(p) !== null || p === '') && parseDateRange(c) === null && lines[i + 3] && parseDateRange(lines[i + 3]); + if (groupedHeader) { + // grouped: a=company, b=type·duration, then (title, range[, location])* + const company = a; + const type = bParts.type; + i += 2; + while (i < lines.length) { + const t = lines[i]; + const r = parseDateRange(lines[i + 1] || ''); + if (!r || parseDateRange(t)) break; + const pos = { title: t, company, type, ...r, duration: parseDuration(r.note), location: '' }; + i += 2; + if (lines[i] && !parseDateRange(lines[i]) && !parseDateRange(lines[i + 1] || '') && /,|remote|hybrid|on-site/i.test(lines[i])) { + pos.location = lines[i]; + i += 1; + } + positions.push(pos); + // next grouped role starts with a title followed by a range; a + // new employer block starts with company then type·duration + const nb = splitCompanyLine(lines[i + 1] || ''); + if (nb.type && !parseDateRange(lines[i + 1] || '') && nb.rest.every((p) => parseDuration(p) !== null || p === '')) break; + } + continue; + } + i += 1; + } + return positions; +} + +export function parseEducation(lines) { + const out = []; + for (let i = 0; i < lines.length; i++) { + const r = parseDateRange(lines[i]); + if (r && i >= 1) { + const degree = i >= 2 && !parseDateRange(lines[i - 2]) && !SECTION_HEADINGS.includes(lines[i - 2].toLowerCase()) ? lines[i - 1] : ''; + const school = degree ? lines[i - 2] : lines[i - 1]; + out.push({ school, degree, start: r.start, end: r.end }); + } + } + return out; +} + +// Full LinkedIn profile parse from captured innerText. Tolerant: anything it +// cannot read is simply absent. +export function parseLinkedInProfile(text) { + const raw = text || ''; + const lines = cleanLines(raw); + const showAll = /show all (\d+) experiences?/i.exec(raw); + const claim = /(\d{1,2})\s*\+?\s*(?:years|yrs)(?:'|’)?(?:\s+of)?(?:\s+(?:professional|industry|hands-on|work))?\s+experience/i.exec(raw.slice(0, 3000)); + const expIdx = lines.findIndex((l) => l.toLowerCase() === 'experience'); + const name = lines[0] && lines[0].length < 80 ? lines[0] : ''; + const headline = lines[1] && lines[1].length < 200 && lines[1].toLowerCase() !== 'about' ? lines[1] : ''; + const location = lines.slice(0, 6).find((l) => /,\s*[A-Za-z ]+$/.test(l) && !l.includes('·') && l !== name && l !== headline) || ''; + const experience = parseExperience(sectionLines(lines, 'Experience')); + const education = parseEducation(sectionLines(lines, 'Education')); + return { + kind: 'linkedin-profile', + name, headline, location, experience, education, + parsed: expIdx >= 0, + listedExperiences: showAll ? Number(showAll[1]) : null, + claimedYears: claim ? Number(claim[1]) : null + }; +} + +// ----- analysis ------------------------------------------------------------ + +const FULL_TIME = (p) => !p.type || p.type.toLowerCase() === 'full-time'; + +function overlapMonths(a, b) { + const s = Math.max(monthIndex(a.start), monthIndex(b.start)); + const e = Math.min(a.end ? monthIndex(a.end, { asEnd: true }) : Infinity, b.end ? monthIndex(b.end, { asEnd: true }) : Infinity); + return e - s + 1; +} + +function tenureMonths(p, now) { + const s = monthIndex(p.start); + const e = p.end ? monthIndex(p.end, { asEnd: true }) : now.getFullYear() * 12 + now.getMonth(); + return Math.max(0, e - s + 1); +} + +// Groups consecutive roles at one employer (promotions) into stints. +export function groupByEmployer(positions) { + const groups = []; + for (const p of positions) { + const key = (p.company || '').toLowerCase(); + const last = groups[groups.length - 1]; + if (last && last.key === key) last.roles.push(p); + else groups.push({ key, company: p.company, roles: [p] }); + } + return groups.map((g) => { + const starts = g.roles.map((r) => monthIndex(r.start)).filter((x) => x !== null); + const ends = g.roles.map((r) => (r.end ? monthIndex(r.end, { asEnd: true }) : Infinity)); + return { ...g, start: Math.min(...starts), end: Math.max(...ends), current: g.roles.some((r) => !r.end) }; + }); +} + +export function analyzeProfile(profile, now = new Date()) { + const findings = []; + const positions = (profile.experience || []).filter((p) => p.start); + if (positions.length === 0) { + return { findings, summary: null }; + } + const nowIdx = now.getFullYear() * 12 + now.getMonth(); + + // 1. impossible ranges + for (const p of positions) { + if (p.end && monthIndex(p.end, { asEnd: true }) < monthIndex(p.start)) { + findings.push({ level: 'ask', code: 'range', text: `${p.title} at ${p.company} ends (${fmtDate(p.end)}) before it starts (${fmtDate(p.start)}).` }); + } + if (monthIndex(p.start) > nowIdx) { + findings.push({ level: 'ask', code: 'future', text: `${p.title} at ${p.company} starts in the future (${fmtDate(p.start)}).` }); + } + } + + // 2. concurrent full-time roles at different employers + const ft = positions.filter(FULL_TIME); + for (let i = 0; i < ft.length; i++) { + for (let j = i + 1; j < ft.length; j++) { + const a = ft[i], b = ft[j]; + if ((a.company || '').toLowerCase() === (b.company || '').toLowerCase()) continue; + const o = overlapMonths(a, b); + if (o >= 2) { + const both = !a.end && !b.end; + findings.push({ + level: 'ask', + code: both ? 'concurrent-present' : 'overlap', + text: both + ? `Two current full-time roles: ${a.title} at ${a.company} and ${b.title} at ${b.company}. Ask which is active and what continues.` + : `${a.title} at ${a.company} (${fmtDate(a.start)}–${fmtDate(a.end)}) overlaps ${b.title} at ${b.company} (${fmtDate(b.start)}–${fmtDate(b.end)}) by about ${o} months. Ask about employment type and transition dates.` + }); + } + } + } + + // 3. tenure, gaps, totals on employer groups + // page order: LinkedIn lists newest first, and the card keeps that order + const groups = groupByEmployer(positions); + const chrono = [...groups].sort((a, b) => a.start - b.start); + for (const g of chrono) { + const months = (g.current ? nowIdx : g.end) - g.start + 1; + if (months < 12 && !g.current) { + findings.push({ level: 'info', code: 'short', text: `${g.company}: about ${months} months.` }); + } + } + for (let i = 1; i < chrono.length; i++) { + const prev = chrono[i - 1]; + const gap = chrono[i].start - prev.end - 1; + if (prev.end !== Infinity && gap >= 6) { + findings.push({ level: 'info', code: 'gap', text: `About ${gap} months between ${prev.company} and ${chrono[i].company}.` }); + } + } + + // truncated capture: the page offered more roles than were visible + if (profile.listedExperiences && profile.listedExperiences > positions.length) { + findings.push({ level: 'info', code: 'truncated', text: `${profile.listedExperiences} experiences listed, ${positions.length} captured. Open "Show all" on the page and capture again for the full history.` }); + } + + // total experience, non-overlapping months + const covered = new Set(); + for (const p of positions) { + const s = monthIndex(p.start); + const e = p.end ? monthIndex(p.end, { asEnd: true }) : nowIdx; + for (let m = s; m <= Math.min(e, nowIdx); m++) covered.add(m); + } + const totalMonths = covered.size; + const completed = groups.filter((g) => !g.current); + const avgTenure = groups.length ? Math.round(groups.reduce((n, g) => n + ((g.current ? nowIdx : g.end) - g.start + 1), 0) / groups.length) : 0; + + // the one-line card: "Company (Title; Title, 2019–2022); Company (Title, 2022–present)" + const card = groups.map((g) => { + // promotions read chronologically: Engineer → Senior Engineer + const titles = [...new Set([...g.roles].sort((a, b) => monthIndex(a.start) - monthIndex(b.start)).map((r) => r.title))].join(' → '); + const first = g.roles.reduce((a, r) => (monthIndex(r.start) < monthIndex(a.start) ? r : a)); + const span = `${first.start.y}–${g.current ? 'present' : Math.floor(g.end / 12)}`; + return `${g.company}: ${titles}, ${span}`; + }).join('; '); + + // a stated "N+ years" that the visible history does not reach + const years = Math.round((totalMonths / 12) * 10) / 10; + if (profile.claimedYears && !(profile.listedExperiences > positions.length) && years < profile.claimedYears - 1) { + findings.push({ level: 'ask', code: 'claim', text: `States ${profile.claimedYears}+ years of experience; the visible history covers about ${years}. Ask what the earlier years were.` }); + } + + const summary = { + years, + employers: groups.length, + averageTenureMonths: avgTenure, + completedStints: completed.length, + currentRoles: positions.filter((p) => !p.end).length, + card: `${Math.round((totalMonths / 12) * 10) / 10} yrs, avg tenure ${Math.round(avgTenure / 12 * 10) / 10} yrs; ${card}` + }; + return { findings, summary }; +} + +// ----- change detection between two parses ---------------------------------- + +function roleKey(p) { + return `${(p.company || '').toLowerCase()}|${(p.title || '').toLowerCase()}`; +} + +// Compares an older parse with a newer one and reports edits to the history: +// changed dates, added or removed roles, changed headline. This is the signal +// that a profile was edited between two visits. +export function diffProfiles(older, newer) { + const changes = []; + if (!older || !newer) return changes; + if (older.headline && newer.headline && older.headline !== newer.headline) { + changes.push({ code: 'headline', text: `Headline changed from "${older.headline}" to "${newer.headline}".` }); + } + const a = new Map((older.experience || []).map((p) => [roleKey(p), p])); + const b = new Map((newer.experience || []).map((p) => [roleKey(p), p])); + for (const [k, p] of a) { + const q = b.get(k); + if (!q) { changes.push({ code: 'removed', text: `Removed: ${p.title} at ${p.company} (${fmtDate(p.start)}–${fmtDate(p.end)}).` }); continue; } + const dates = (x) => `${fmtDate(x.start)}–${fmtDate(x.end)}`; + if (dates(p) !== dates(q)) changes.push({ code: 'dates', text: `${p.title} at ${p.company}: dates were ${dates(p)}, now ${dates(q)}.` }); + if ((p.type || '') !== (q.type || '') && (p.type || q.type)) changes.push({ code: 'type', text: `${p.title} at ${p.company}: employment type changed from ${p.type || 'unspecified'} to ${q.type || 'unspecified'}.` }); + } + for (const [k, q] of b) { + if (!a.has(k)) changes.push({ code: 'added', text: `Added: ${q.title} at ${q.company} (${fmtDate(q.start)}–${fmtDate(q.end)}).` }); + } + return changes; +} + +// ----- generic text diff for any page --------------------------------------- + +export function diffLines(oldText, newText) { + const a = cleanLines(oldText || ''); + const b = cleanLines(newText || ''); + const setA = new Set(a); + const setB = new Set(b); + return { + removed: a.filter((l) => !setB.has(l)), + added: b.filter((l) => !setA.has(l)), + same: a.filter((l) => setB.has(l)).length + }; +} + +// Which extractor applies to a page, by rule name, and how much text its +// section needs. Add a parser here and in test/fixtures first. +const EXTRACTORS = { + 'LinkedIn profile': { parse: parseLinkedInProfile, maxChars: 40000 } +}; + +export function extractorFor(ruleName) { + return EXTRACTORS[ruleName]?.parse || null; +} + +export function captureBudget(ruleName) { + return EXTRACTORS[ruleName]?.maxChars || 6000; +} diff --git a/src/lib/settings.js b/src/lib/settings.js new file mode 100644 index 0000000..6c676b0 --- /dev/null +++ b/src/lib/settings.js @@ -0,0 +1,58 @@ +// Settings persistence: chrome.storage.sync with a local fallback, the newer +// copy wins on load, plus JSON export/import of settings (notes export is +// separate, see store.js and notes.js). + +import { DEFAULT_NORMALIZATION, DEFAULT_RULES } from './url.js'; + +// How the toolbar icon opens the note: a popup beside the icon, the docked +// side panel, or a floating button on the page itself. +export const LAUNCH_MODES = ['popup', 'sidebar', 'floating']; + +export const DEFAULTS = { + showBadge: true, + openTabOnReminder: true, + snapshotOnSave: true, + writeToVault: true, + launch: 'popup', + normalization: { ...DEFAULT_NORMALIZATION }, + rules: DEFAULT_RULES.map((r) => ({ ...r, enabled: true })) +}; + +const KEYS = ['settings', 'savedAt']; + +function area(name) { + return globalThis.chrome && chrome.storage && chrome.storage[name] ? chrome.storage[name] : null; +} +async function read(name) { + const store = area(name); + if (!store) return {}; + try { return await store.get(KEYS); } catch (e) { return {}; } +} +async function write(name, data) { + const store = area(name); + if (!store) return false; + try { await store.set(data); return true; } catch (e) { return false; } +} + +export function mergeSettings(stored) { + const s = { ...DEFAULTS, ...(stored || {}) }; + s.normalization = { ...DEFAULT_NORMALIZATION, ...((stored && stored.normalization) || {}) }; + s.rules = Array.isArray(stored?.rules) ? stored.rules : DEFAULTS.rules.map((r) => ({ ...r })); + // the earlier boolean "open in the sidebar" becomes the sidebar launch mode + s.launch = LAUNCH_MODES.includes(stored?.launch) ? stored.launch : stored?.openInSidePanel ? 'sidebar' : 'popup'; + delete s.openInSidePanel; + return s; +} + +export async function loadSettings() { + const [sync, local] = await Promise.all([read('sync'), read('local')]); + const pick = sync.settings && local.settings ? ((local.savedAt || 0) > (sync.savedAt || 0) ? local : sync) : sync.settings ? sync : local; + return { settings: mergeSettings(pick.settings), source: pick === local ? 'local' : 'sync' }; +} + +export async function saveSettings(settings) { + const payload = { settings: mergeSettings(settings), savedAt: Date.now() }; + const ok = await write('sync', payload); + await write('local', payload); + return ok ? 'sync' : 'local'; +} diff --git a/src/lib/share.js b/src/lib/share.js new file mode 100644 index 0000000..b447f04 --- /dev/null +++ b/src/lib/share.js @@ -0,0 +1,122 @@ +// Share stickies (and optionally the notes) for one page as a flat text file, +// no server: JSON, deflate-compressed, optionally encrypted with a passphrase +// (PBKDF2-SHA256 -> AES-256-GCM), base64, wrapped so it survives email and +// chat. Pure Web APIs only, so it runs in the worker, the pages and node. +// +// -----BEGIN MARGINALIA----- +// marginalia/1 deflate aes-gcm +// +// -----END MARGINALIA----- + +export const SHARE_BEGIN = '-----BEGIN MARGINALIA-----'; +export const SHARE_END = '-----END MARGINALIA-----'; +const VERSION = 'marginalia/1'; +const PBKDF2_ITERATIONS = 250000; + +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +export function sharePayload(note, { includeEntries = false, now = new Date() } = {}) { + return { + v: 1, + url: note.url, + key: note.key, + title: note.title, + host: note.host, + sharedAt: now.toISOString(), + stickies: (note.stickies || []).map(({ id, anchor, text, created, updated }) => ({ id, anchor, text, created, updated })), + ...(includeEntries ? { entries: note.entries.map(({ at, text }) => ({ at, text })) } : {}) + }; +} + +async function pipe(bytes, stream) { + const out = new Blob([bytes]).stream().pipeThrough(stream); + return new Uint8Array(await new Response(out).arrayBuffer()); +} +const compress = (bytes) => pipe(bytes, new CompressionStream('deflate-raw')); +const decompress = (bytes) => pipe(bytes, new DecompressionStream('deflate-raw')); + +function toBase64(bytes) { + let s = ''; + for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + return btoa(s); +} +function fromBase64(s) { + const bin = atob(s.replace(/\s+/g, '')); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +async function deriveKey(passphrase, salt) { + const material = await crypto.subtle.importKey('raw', enc.encode(passphrase.normalize('NFKC')), 'PBKDF2', false, ['deriveKey']); + return crypto.subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-256', salt, iterations: PBKDF2_ITERATIONS }, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); +} + +export async function encodeShare(payload, { passphrase = '' } = {}) { + let bytes = await compress(enc.encode(JSON.stringify(payload))); + let cipher = 'none'; + if (passphrase) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(passphrase, salt); + const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, bytes)); + bytes = new Uint8Array(salt.length + iv.length + ct.length); + bytes.set(salt, 0); bytes.set(iv, salt.length); bytes.set(ct, salt.length + iv.length); + cipher = 'aes-gcm'; + } + const b64 = toBase64(bytes).replace(/(.{76})/g, '$1\n').trimEnd(); + return [SHARE_BEGIN, `${VERSION} deflate ${cipher}`, b64, SHARE_END, ''].join('\n'); +} + +// What a text claims to be, before decoding: { cipher } or null if not ours. +export function inspectShare(text) { + const m = /-----BEGIN MARGINALIA-----\s*\n\s*marginalia\/(\d+) (\S+) (\S+)\s*\n([\s\S]*?)-----END MARGINALIA-----/.exec(text || ''); + if (!m) return null; + return { version: Number(m[1]), compression: m[2], cipher: m[3], body: m[4] }; +} + +export async function decodeShare(text, { passphrase = '' } = {}) { + const info = inspectShare(text); + if (!info) throw new Error('This is not a Marginalia share.'); + if (info.version !== 1 || info.compression !== 'deflate') throw new Error('This share needs a newer version of the extension.'); + let bytes = fromBase64(info.body); + if (info.cipher === 'aes-gcm') { + if (!passphrase) throw new Error('This share is encrypted. Enter the passphrase.'); + const salt = bytes.slice(0, 16), iv = bytes.slice(16, 28), ct = bytes.slice(28); + const key = await deriveKey(passphrase, salt); + try { bytes = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct)); } catch (e) { throw new Error('Wrong passphrase.'); } + } else if (info.cipher !== 'none') throw new Error(`Unknown encryption "${info.cipher}".`); + let payload; + try { payload = JSON.parse(dec.decode(await decompress(bytes))); } catch (e) { throw new Error('The share is damaged.'); } + if (!payload || payload.v !== 1 || !payload.url || !payload.key || !Array.isArray(payload.stickies)) throw new Error('The share is damaged.'); + return payload; +} + +// Merge a received payload into the recipient's note for that page: stickies +// by id, entries by time and text, nothing overwritten. +export function applyShare(note, payload, now = new Date()) { + const iso = now.toISOString(); + const stickies = [...(note.stickies || [])]; + let addedStickies = 0; + for (const s of payload.stickies) { + if (!s || !s.id || stickies.some((x) => x.id === s.id)) continue; + stickies.push({ id: s.id, anchor: s.anchor || null, text: s.text || '', created: s.created || iso, updated: s.updated || iso }); + addedStickies++; + } + const entries = [...note.entries]; + let addedEntries = 0; + const seen = new Set(entries.map((e) => `${e.at.slice(0, 16)}|${e.text}`)); + for (const e of payload.entries || []) { + if (!e?.text || seen.has(`${String(e.at).slice(0, 16)}|${e.text}`)) continue; + entries.push({ at: e.at || iso, text: e.text }); + addedEntries++; + } + entries.sort((a, b) => a.at.localeCompare(b.at)); + const changed = addedStickies || addedEntries; + return { note: changed ? { ...note, stickies, entries, updated: iso } : note, addedStickies, addedEntries }; +} + +export function shareFileName(note) { + return `${(note.title || 'page').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50) || 'page'}-stickies.marginalia.txt`; +} diff --git a/src/lib/store.js b/src/lib/store.js new file mode 100644 index 0000000..ff54afc --- /dev/null +++ b/src/lib/store.js @@ -0,0 +1,157 @@ +// Notes live in chrome.storage.local (sync is capped at 100 KB total, far too +// small for a notes habit). One item per note plus a small index for lists. +// Settings are in settings.js (sync). + +import { handleFolder } from './vault.js'; +import { indexEntry, sanitizeSavedTitle } from './notes.js'; + +const INDEX_KEY = 'notes:index'; +const noteKey = (id) => `note:${id}`; + +// The index is one item that several handlers (popup, alarms, context menu) +// read-modify-write; serialise those writes so none are lost. +let chain = Promise.resolve(); +function serialized(fn) { + const run = chain.then(fn, fn); + chain = run.catch(() => {}); + return run; +} + +export async function getNote(id) { + const data = await chrome.storage.local.get(noteKey(id)); + return data[noteKey(id)] || null; +} + +export async function getIndex() { + const data = await chrome.storage.local.get(INDEX_KEY); + return data[INDEX_KEY] || {}; +} + +export function putNote(note) { + return serialized(() => putNoteNow(note)); +} + +export function deleteNote(id) { + return serialized(() => deleteNoteNow(id)); +} + +export function migrateNoteTitles() { + return serialized(async () => { + const marker = 'migration:linkedin-titles:1'; + if ((await chrome.storage.local.get(marker))[marker]) return; + const data = await chrome.storage.local.get(null); + const index = { ...(data[INDEX_KEY] || {}) }; + const changes = {}; + for (const [key, note] of Object.entries(data)) { + if (!key.startsWith('note:') || !note?.id) continue; + const next = sanitizeSavedTitle(note); + if (next === note) continue; + changes[key] = next; + index[note.id] = indexEntry(next); + } + await chrome.storage.local.set({ ...changes, [INDEX_KEY]: index, [marker]: true }); + }); +} + +async function putNoteNow(note) { + const index = await getIndex(); + if (note.entries.length === 0 && !note.remind && !(note.snapshots && note.snapshots.length) && !(note.stickies && note.stickies.length) && !(note.links && note.links.length)) { + // a note with no entries, reminder, captures, stickies or links is a deleted note + delete index[note.id]; + await chrome.storage.local.remove(noteKey(note.id)); + await chrome.storage.local.set({ [INDEX_KEY]: index }); + return null; + } + index[note.id] = indexEntry(note); + await chrome.storage.local.set({ [noteKey(note.id)]: note, [INDEX_KEY]: index }); + return note; +} + +async function deleteNoteNow(id) { + const index = await getIndex(); + delete index[id]; + await chrome.storage.local.remove(noteKey(id)); + await chrome.storage.local.set({ [INDEX_KEY]: index }); +} + +export async function allNotes() { + const index = await getIndex(); + const ids = Object.keys(index); + if (ids.length === 0) return []; + const data = await chrome.storage.local.get(ids.map(noteKey)); + return ids.map((id) => data[noteKey(id)]).filter(Boolean); +} + +// Rebuilds the index from the notes themselves (after import, or if it drifts). +export async function rebuildIndex() { + const all = await chrome.storage.local.get(null); + const index = {}; + for (const [k, v] of Object.entries(all)) { + if (k.startsWith('note:') && v && v.id) index[v.id] = indexEntry(v); + } + await chrome.storage.local.set({ [INDEX_KEY]: index }); + return index; +} + +// ----- Obsidian vault handle (File System Access API) --------------------- +// The directory handle is stored in IndexedDB; it is usable from the service +// worker once the user has granted permission from the options page. + +const DB = 'page-notes'; +const STORE = 'handles'; + +function openDb() { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB, 1); + req.onupgradeneeded = () => req.result.createObjectStore(STORE); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +export async function saveVaultHandle(handle) { + const db = await openDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).put(handle, 'vault'); + tx.oncomplete = resolve; + tx.onerror = () => reject(tx.error); + }); +} + +export async function loadVaultHandle() { + try { + const db = await openDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readonly'); + const req = tx.objectStore(STORE).get('vault'); + req.onsuccess = () => resolve(req.result || null); + req.onerror = () => reject(req.error); + }); + } catch (e) { + return null; + } +} + +export async function clearVaultHandle() { + const db = await openDb(); + await new Promise((resolve) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).delete('vault'); + tx.oncomplete = resolve; + }); +} + +// The vault folder as a sync adapter (see vault.js), if we hold permission. +// status: 'ok' | 'no-vault' | 'no-permission' | 'error'. +export async function vaultFolder() { + const handle = await loadVaultHandle(); + if (!handle) return { status: 'no-vault', folder: null }; + try { + if ((await handle.queryPermission({ mode: 'readwrite' })) !== 'granted') return { status: 'no-permission', folder: null }; + return { status: 'ok', folder: handleFolder(handle) }; + } catch (e) { + console.warn('vault unavailable', e); + return { status: 'error', folder: null }; + } +} diff --git a/src/lib/ui.js b/src/lib/ui.js new file mode 100644 index 0000000..3594a71 --- /dev/null +++ b/src/lib/ui.js @@ -0,0 +1,32 @@ +// Small DOM and messaging helpers shared by the popup, options and note pages. + +export function send(type, args = {}) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type, args }, (r) => { + if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message)); + else if (!r || !r.ok) reject(new Error(r?.error || 'No response from the extension')); + else resolve(r.result); + }); + }); +} + +export function el(tag, attrs = {}, children = []) { + const node = document.createElement(tag); + for (const [k, v] of Object.entries(attrs)) { + if (k === 'class') node.className = v; + else if (k === 'text') node.textContent = v; + else if (k.startsWith('on')) node.addEventListener(k.slice(2), v); + else if (v !== undefined && v !== null && v !== false) node.setAttribute(k, v === true ? '' : v); + } + for (const c of [].concat(children)) { + if (c !== null && c !== undefined) node.append(c.nodeType ? c : document.createTextNode(c)); + } + return node; +} + +// Local wall-clock stamp: YYYY-MM-DD HH:MM +export function fmt(iso) { + const d = new Date(iso); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} diff --git a/src/lib/url.js b/src/lib/url.js new file mode 100644 index 0000000..2bdba6f --- /dev/null +++ b/src/lib/url.js @@ -0,0 +1,491 @@ +// Pure URL logic: normalization, entity rules, duplicate detection, sorting. +// No Chrome APIs here so it can be unit tested with `node --test`. + +export const TRACKING_PARAMS = [ + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'utm_id', + 'fbclid', + 'gclid', + 'dclid', + 'msclkid', + 'mc_cid', + 'mc_eid', + 'igshid', + 'ref', + 'ref_src', + 'ref_url', + 'si', + '_hsenc', + '_hsmi', + 'hsCtaTracking', + 'vero_id', + 'yclid', + 'trk', + 'trkInfo', + 'ocid', + 's', + 'feature' +]; + +// Built-in entity rules. `match` is a regex source tested against the raw URL, +// `key` is a template where $1..$9 refer to capture groups. Two tabs whose URLs +// produce the same key are considered duplicates even if the URLs differ. +export const DEFAULT_RULES = [ + { + name: 'GitHub pull request', + match: '^https?://github\\.com/([^/]+)/([^/]+)/pull/(\\d+)', + key: 'github:$1/$2/pull/$3' + }, + { + name: 'GitHub issue', + match: '^https?://github\\.com/([^/]+)/([^/]+)/issues/(\\d+)', + key: 'github:$1/$2/issues/$3' + }, + { + name: 'Bitbucket pull request', + match: '^https?://bitbucket\\.org/([^/]+)/([^/]+)/pull-requests/(\\d+)', + key: 'bitbucket:$1/$2/pr/$3' + }, + { + name: 'GitLab merge request', + match: '^https?://gitlab\\.com/(.+?)/-/merge_requests/(\\d+)', + key: 'gitlab:$1/mr/$2' + }, + { + name: 'Jira issue', + match: '^https?://([^/]+\\.atlassian\\.net)/(?:browse/|.*[?&]selectedIssue=)([A-Z][A-Z0-9]+-\\d+)', + key: 'jira:$1/$2' + }, + { + name: 'Confluence page', + match: '^https?://([^/]+\\.atlassian\\.net)/wiki/spaces/[^/]+/pages/(\\d+)', + key: 'confluence:$1/$2' + }, + { + name: 'LinkedIn profile', + match: '^https?://(?:[a-z]+\\.)?linkedin\\.com/in/([^/?#]+)', + key: 'linkedin:in/$1' + }, + { + name: 'LinkedIn company', + match: '^https?://(?:[a-z]+\\.)?linkedin\\.com/company/([^/?#]+)', + key: 'linkedin:company/$1' + }, + { + name: 'YouTube video', + match: '^https?://(?:(?:www|m)\\.youtube\\.com/watch\\?(?:.*&)?v=|youtu\\.be/)([\\w-]{6,})', + key: 'youtube:$1' + }, + { + name: 'Google Docs / Sheets / Slides', + match: '^https?://docs\\.google\\.com/(document|spreadsheets|presentation|forms)/d/([^/?#]+)', + key: 'gdocs:$1/$2' + }, + { + name: 'Google Drive file', + match: '^https?://drive\\.google\\.com/(?:file/d/|drive/folders/)([^/?#]+)', + key: 'gdrive:$1' + }, + { + name: 'Notion page', + match: '^https?://(?:www\\.)?(?:[^/]+\\.)?notion\\.(?:so|site|com)/(?:[^/?#]+/)*?[^/?#]*?([0-9a-f]{32})(?:[?#/]|$)', + key: 'notion:$1' + }, + { + name: 'HubSpot record', + match: '^https?://app(?:-[a-z0-9]+)?\\.hubspot\\.com/contacts/(\\d+)/(?:record/)?([\\w-]+)/(\\d+)', + key: 'hubspot:$1/$2/$3' + }, + { + name: 'Slack channel or thread', + match: '^https?://app\\.slack\\.com/client/([A-Z0-9]+)/([A-Z0-9]+)(?:/thread/[A-Z0-9]+-(\\d+\\.\\d+))?', + key: 'slack:$1/$2/$3' + }, + { + name: 'Figma file', + match: '^https?://(?:www\\.)?figma\\.com/(?:file|design|board|proto)/([\\w-]+)', + key: 'figma:$1' + }, + { + name: 'Amazon product', + match: '^https?://(?:www\\.)?amazon\\.[a-z.]+/(?:.*/)?(?:dp|gp/product)/([A-Z0-9]{10})', + key: 'amazon:$1' + }, + { + name: 'Stack Overflow question', + match: '^https?://stackoverflow\\.com/(?:questions|q)/(\\d+)', + key: 'stackoverflow:$1' + }, + { + name: 'Reddit post', + match: '^https?://(?:www|old|new)\\.reddit\\.com/r/([^/]+)/comments/([a-z0-9]+)', + key: 'reddit:$1/$2' + }, + { + name: 'Twitter / X post', + match: '^https?://(?:www\\.)?(?:twitter|x)\\.com/[^/]+/status/(\\d+)', + key: 'x:$1' + } +]; + +// Pages that are a meeting or a media player even when silent (muted call, +// paused video). Audible tabs count as media regardless of URL. +export const DEFAULT_MEDIA_PATTERNS = [ + '^https?://meet\\.google\\.com/[a-z]{3}-[a-z]{4}-[a-z]{3}', + '^https?://[^/]*zoom\\.us/(j|wc|s|my)/', + '^https?://teams\\.(microsoft|live)\\.com/.*(meetup-join|/meet/|/v2/)', + '^https?://[^/]*whereby\\.com/', + '^https?://[^/]*around\\.co/', + '^https?://[^/]*webex\\.com/(meet|join|wbxmjs)', + '^https?://app\\.gather\\.town/', + '^https?://discord\\.com/channels/', + '^https?://app\\.slack\\.com/huddle/', + '^https?://(www|m)\\.youtube\\.com/watch', + '^https?://music\\.youtube\\.com/', + '^https?://[^/]*twitch\\.tv/', + '^https?://[^/]*vimeo\\.com/\\d+', + '^https?://open\\.spotify\\.com/', + '^https?://[^/]*netflix\\.com/watch/', + '^https?://[^/]*loom\\.com/share/' +]; + +export function compileMediaPatterns(patterns) { + const compiled = []; + for (const p of patterns || []) { + try { + compiled.push(new RegExp(p, 'i')); + } catch (e) { + // ignore bad patterns; the options page reports them + } + } + return compiled; +} + +export function isMediaUrl(url, compiledMedia = []) { + return !!url && compiledMedia.some((re) => re.test(url)); +} + +// A media tab is audible, or on a meeting/player page even while silent. +export function isMediaTab(tab, compiledMedia = []) { + return !!tab.audible || isMediaUrl(tab.url || tab.pendingUrl || '', compiledMedia); +} + +export const DEFAULT_NORMALIZATION = { + ignoreScheme: true, + stripWww: true, + ignoreFragment: true, + ignoreTrailingSlash: true, + stripTrackingParams: true, + sortQueryParams: true, + trackingParams: TRACKING_PARAMS +}; + +export const BLANK_URLS = new Set([ + 'chrome://newtab/', + 'chrome://new-tab-page/', + 'chrome://new-tab-page-third-party/', + 'brave://newtab/', + 'edge://newtab/', + 'about:blank', + 'about:newtab', + '' +]); + +export function isBlankUrl(url) { + return BLANK_URLS.has(url || ''); +} + +export function parseUrl(url) { + try { + return new URL(url); + } catch (e) { + return null; + } +} + +export function isWebUrl(url) { + const u = parseUrl(url); + return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); +} + +// Hostname without a leading www., or null for non-web URLs. +export function hostOf(url) { + const u = parseUrl(url); + if (!u || !u.hostname) { + return null; + } + return u.hostname.toLowerCase().replace(/^www\./, ''); +} + +// Host plus first path segment, e.g. github.com/dataro. Falls back to the host. +export function sectionOf(url) { + const u = parseUrl(url); + if (!u || !u.hostname) { + return null; + } + const host = u.hostname.toLowerCase().replace(/^www\./, ''); + const first = u.pathname.split('/').filter(Boolean)[0]; + return first ? `${host}/${first}` : host; +} + +export function normalizeUrl(url, options = {}) { + const opts = { ...DEFAULT_NORMALIZATION, ...options }; + const u = parseUrl(url); + if (!u) { + return url || ''; + } + if (u.protocol !== 'http:' && u.protocol !== 'https:') { + return opts.ignoreFragment ? url.split('#')[0] : url; + } + + if (opts.ignoreScheme) { + u.protocol = 'https:'; + } + u.hostname = u.hostname.toLowerCase(); + if (opts.stripWww) { + u.hostname = u.hostname.replace(/^www\./, ''); + } + if (opts.ignoreFragment) { + u.hash = ''; + } + if (opts.stripTrackingParams || opts.sortQueryParams) { + const tracking = new Set( + (opts.trackingParams || []).map((p) => p.toLowerCase()) + ); + const kept = []; + for (const [k, v] of u.searchParams.entries()) { + if (opts.stripTrackingParams && tracking.has(k.toLowerCase())) { + continue; + } + kept.push([k, v]); + } + if (opts.sortQueryParams) { + kept.sort((a, b) => + a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : 1 + ); + } + const params = new URLSearchParams(); + kept.forEach(([k, v]) => params.append(k, v)); + const qs = params.toString(); + u.search = qs ? `?${qs}` : ''; + } + let out = u.toString(); + if (opts.ignoreTrailingSlash) { + // strip a trailing slash on the path, but keep the root "/" + out = out.replace(/\/(\?[^#]*)?$/, (m, q) => + u.pathname === '/' ? m : q || '' + ); + } + return out; +} + +// Compile rule definitions into regexes. Invalid rules are skipped and reported +// via the returned `errors` array so the options page can show them. +export function compileRules(rules) { + const compiled = []; + const errors = []; + (rules || []).forEach((rule, i) => { + if (!rule || rule.enabled === false) { + return; + } + if (!rule.match || !rule.key) { + errors.push({ index: i, rule, error: 'match and key are required' }); + return; + } + try { + compiled.push({ + name: rule.name || `Rule ${i + 1}`, + re: new RegExp(rule.match, rule.flags || 'i'), + key: rule.key + }); + } catch (e) { + errors.push({ index: i, rule, error: e.message }); + } + }); + return { compiled, errors }; +} + +export function expandTemplate(template, match) { + return template + .replace(/\$\{(\w+)\}/g, (m, name) => + match.groups && match.groups[name] !== undefined + ? match.groups[name] + : '' + ) + .replace(/\$(\d)/g, (m, n) => match[Number(n)] ?? ''); +} + +// Returns { key, rule } where rule is the matching rule name or null when only +// generic normalization applied. +export function canonicalize(url, compiledRules = [], normalization = {}) { + if (!url) { + return { key: '', rule: null }; + } + for (const rule of compiledRules) { + const match = rule.re.exec(url); + if (match) { + return { + key: expandTemplate(rule.key, match).toLowerCase(), + rule: rule.name + }; + } + } + return { key: normalizeUrl(url, normalization), rule: null }; +} + +export function matchingRule(url, compiledRules = []) { + for (const rule of compiledRules) { + if (rule.re.test(url)) { + return rule; + } + } + return null; +} + +// Which tab of a duplicate set to keep. Pinned beats active beats the policy. +export function survivorScore(tab, policy = 'recent') { + let score = 0; + if (tab.pinned) { + score += 1e15; + } + if (tab.active) { + score += 1e14; + } + if (policy === 'recent') { + score += tab.lastAccessed || 0; + } else if (policy === 'deepest') { + score += (tab.url || '').length; + } else if (policy === 'first') { + score -= (tab.windowId || 0) * 1e6 + (tab.index || 0); + } + return score; +} + +// Groups tabs by canonical key. Returns sets with more than one tab, each with +// a `keep` tab and `close` tabs. +export function findDuplicates(tabs, compiledRules, normalization, policy) { + const byKey = new Map(); + for (const tab of tabs) { + const url = tab.url || tab.pendingUrl || ''; + if (!url || isBlankUrl(url)) { + continue; + } + const { key, rule } = canonicalize(url, compiledRules, normalization); + // never treat a normal and an incognito tab as duplicates of each other + const scoped = `${tab.incognito ? 'i:' : 'n:'}${key}`; + if (!byKey.has(scoped)) { + byKey.set(scoped, { key, rule, tabs: [] }); + } + byKey.get(scoped).tabs.push(tab); + } + const sets = []; + for (const entry of byKey.values()) { + if (entry.tabs.length < 2) { + continue; + } + const sorted = [...entry.tabs].sort( + (a, b) => survivorScore(b, policy) - survivorScore(a, policy) + ); + sets.push({ + key: entry.key, + rule: entry.rule, + keep: sorted[0], + close: sorted.slice(1) + }); + } + return sets; +} + +// Describes the scopes a "close tabs like this one" action can use for a URL. +export function scopesFor(url, compiledRules = []) { + const host = hostOf(url); + if (!host) { + return []; + } + const scopes = [{ id: 'domain', label: host, test: (u) => hostOf(u) === host }]; + const section = sectionOf(url); + if (section && section !== host) { + scopes.push({ + id: 'section', + label: section, + test: (u) => sectionOf(u) === section + }); + } + const rule = matchingRule(url, compiledRules); + if (rule) { + scopes.push({ + id: 'entity', + label: `${rule.name}s`, + test: (u) => rule.re.test(u) + }); + } + return scopes; +} + +// Sort key that clusters related hosts: reversed host labels, then path, +// then query. "docs.google.com" sorts next to "mail.google.com". +export function sortKey(url) { + const u = parseUrl(url); + if (!u || !u.hostname) { + return `~${url || ''}`; + } + const host = u.hostname.toLowerCase().replace(/^www\./, ''); + const reversed = host.split('.').reverse().join('.'); + return `${reversed} ${u.pathname.toLowerCase()} ${u.search}`; +} + +export function compareTabs(a, b) { + const ka = sortKey(a.url || a.pendingUrl); + const kb = sortKey(b.url || b.pendingUrl); + return ka < kb ? -1 : ka > kb ? 1 : 0; +} + +export function isStale(tab, days, now = Date.now()) { + if (!tab.lastAccessed || days <= 0) { + return days <= 0 ? !tab.active : false; + } + return now - tab.lastAccessed > days * 86400000; +} + +// A short label for a tab group built from a host: "github" for github.com, +// "atlassian" for dataro.atlassian.net. +export function groupTitleForHost(host) { + if (!host) { + return ''; + } + const parts = host.split('.'); + if (parts.length <= 2) { + return parts[0]; + } + // drop common public suffix parts (co.uk, com.au ...) and keep the brand + const suffixes = new Set(['co', 'com', 'org', 'net', 'gov', 'edu', 'ac']); + let i = parts.length - 2; + if (suffixes.has(parts[i]) && parts[i + 1].length === 2 && i > 0) { + i -= 1; + } + return parts[i]; +} + +export const GROUP_COLORS = [ + 'grey', + 'blue', + 'red', + 'yellow', + 'green', + 'pink', + 'purple', + 'cyan', + 'orange' +]; + +export function colorForHost(host) { + let hash = 0; + for (const ch of host || '') { + hash = (hash * 31 + ch.charCodeAt(0)) >>> 0; + } + return GROUP_COLORS[hash % GROUP_COLORS.length]; +} diff --git a/src/lib/vault.js b/src/lib/vault.js new file mode 100644 index 0000000..ce8d0a2 --- /dev/null +++ b/src/lib/vault.js @@ -0,0 +1,226 @@ +// Two-way vault sync: the Markdown files in the vault folder are the shared +// truth between browser profiles; this module reconciles one profile's notes +// with them. Pure: the folder and the note store come in as adapters, so the +// same code runs over a real FileSystemDirectoryHandle and over an in-memory +// folder in tests. +// +// folder: { list() -> [{ name, mtime, size }], read(name) -> text, +// write(name, text) -> { mtime, size }, remove(name), +// stat(name) -> { name, mtime, size } | null (optional, for scoped syncs) } +// store: { allNotes() -> [note], putNote(note) -> note|null, deleteNote(id) } +// state: { files: { [name]: { id, mtime, size, hash } } } (per profile) +// +// Fast by design: the scan reads only file metadata; a file's text is read +// when its mtime or size changed, and compared by hash before parsing, since +// cloud drives touch mtimes without changing content. Nothing is written when +// a file already equals the note's serialization, so two profiles converge +// instead of ping-ponging. A file watcher (FileSystemObserver, where the +// browser has it) names the files that changed, and `only` scopes the sync to +// them; the periodic full scan remains the safety net. + +import { fileNameFor, fromMarkdown, mergeNotes, toMarkdown } from './notes.js'; + +export const SYNC_ALARM = 'vault:sync'; +export const SYNC_PERIOD_MINUTES = 5; +export const STATE_KEY = 'vault:state'; +export const STATUS_KEY = 'vault:status'; + +// FNV-1a, 32-bit, hex: cheap and good enough to tell "same text" apart. +export function hashText(s) { + let h = 0x811c9dc5; + for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } + return (h >>> 0).toString(16).padStart(8, '0'); +} + +export function emptyState() { return { files: {} }; } + +// A note with nothing a person wrote is not worth a file. +export function worthAFile(note) { + return !!(note.entries?.length || note.stickies?.length || note.links?.length || note.remind); +} + +const recordFor = (state, id) => Object.entries(state.files).find(([, r]) => r.id === id); + +export async function syncFolder(folder, store, state = emptyState(), { now = new Date(), only = null } = {}) { + state = { files: { ...(state?.files || {}) } }; + const scoped = only instanceof Set && only.size > 0; + const report = { scanned: 0, read: 0, imported: 0, updated: 0, written: 0, deletedLocal: 0, deletedFiles: 0, errors: [], scoped }; + let files; + if (scoped && folder.stat) files = (await Promise.all([...only].map((n) => folder.stat(n)))).filter(Boolean); + else files = await folder.list(); + files = files.filter((f) => f.name.endsWith('.md') && (!scoped || only.has(f.name))); + report.scanned = files.length; + const notes = new Map((await store.allNotes()).map((n) => [n.id, n])); + const seen = new Set(); + + // Write a note's serialization under its current name, retiring the old + // name if the title moved it, and record what the folder now holds. + async function writeBack(note, text, fromName) { + const name = fileNameFor(note); + const meta = await folder.write(name, text); + report.written++; + if (fromName && fromName !== name) { await folder.remove(fromName).catch(() => {}); delete state.files[fromName]; } + state.files[name] = { id: note.id, mtime: meta.mtime, size: meta.size, hash: hashText(text) }; + } + + for (const file of files) { + const rec = state.files[file.name]; + try { + // A file we synced before, for a note this profile no longer has: + // the note was deleted here. The file goes. + if (rec && !notes.has(rec.id)) { + await folder.remove(file.name); + delete state.files[file.name]; + report.deletedFiles++; + continue; + } + if (rec && rec.mtime === file.mtime && rec.size === file.size) { + // the file is as we left it; this profile may have changed the + // note since (a save while the vault was unreachable) + seen.add(rec.id); + const local = notes.get(rec.id); + const canonical = toMarkdown(local); + if (hashText(canonical) !== rec.hash) await writeBack(local, canonical, file.name); + continue; + } + const text = await folder.read(file.name); + report.read++; + const hash = hashText(text); + if (rec && rec.hash === hash) { state.files[file.name] = { ...rec, mtime: file.mtime, size: file.size }; seen.add(rec.id); continue; } + let incoming; + try { incoming = fromMarkdown(text); } catch (e) { report.errors.push(`${file.name}: ${e.message}`); continue; } + seen.add(incoming.id); + const existing = notes.get(incoming.id) || null; + let note = existing ? mergeNotes(existing, incoming, now) : incoming; + if (!existing || toMarkdown(note) !== toMarkdown(existing)) { + note = (await store.putNote(note)) || note; + notes.set(note.id, note); + existing ? report.updated++ : report.imported++; + } else note = existing; + // The file lacks what this profile has: write the union back, under + // the note's current file name. + const canonical = toMarkdown(note); + if (canonical !== text || fileNameFor(note) !== file.name) await writeBack(note, canonical, file.name); + else state.files[file.name] = { id: note.id, mtime: file.mtime, size: file.size, hash }; + } catch (e) { + report.errors.push(`${file.name}: ${e.message}`); + } + } + + const present = new Set(files.map((f) => f.name)); + if (scoped) { + // a reported file that is gone: another profile deleted that note + for (const name of only) { + const rec = state.files[name]; + if (present.has(name) || !rec) continue; + try { + if (notes.has(rec.id)) { await store.deleteNote(rec.id); report.deletedLocal++; } + delete state.files[name]; + } catch (e) { report.errors.push(`${name}: ${e.message}`); } + } + return { state, report }; + } + + // Notes without a file: new here, so write them; or synced before and the + // file is gone, so another profile deleted them. + for (const note of notes.values()) { + if (seen.has(note.id)) continue; + const rec = recordFor(state, note.id); + try { + if (rec && !present.has(rec[0])) { + await store.deleteNote(note.id); + delete state.files[rec[0]]; + report.deletedLocal++; + } else if (!rec && worthAFile(note)) { + await writeBack(note, toMarkdown(note), null); + } + } catch (e) { + report.errors.push(`${fileNameFor(note)}: ${e.message}`); + } + } + // records for files that are gone and notes that are gone: forget them + for (const [name, rec] of Object.entries(state.files)) if (!present.has(name) && !notes.has(rec.id)) delete state.files[name]; + return { state, report }; +} + +// An in-memory folder: tests, and the e2e seam (`args.folder`). +export function memoryFolder(files = {}, clock = () => Date.now()) { + const store = { ...files }; + return { + files: store, + async list() { return Object.entries(store).map(([name, f]) => ({ name, mtime: f.mtime, size: f.text.length })); }, + async stat(name) { return store[name] ? { name, mtime: store[name].mtime, size: store[name].text.length } : null; }, + async read(name) { if (!store[name]) throw new Error('No such file'); return store[name].text; }, + async write(name, text) { const mtime = Math.max(clock(), (store[name]?.mtime || 0) + 1); store[name] = { text, mtime }; return { mtime, size: text.length }; }, + async remove(name) { delete store[name]; } + }; +} + +// A folder over a FileSystemDirectoryHandle (extension pages and the worker). +export function handleFolder(handle) { + return { + async list() { + const out = []; + for await (const [name, h] of handle.entries()) { + if (h.kind !== 'file' || !name.endsWith('.md')) continue; + const f = await h.getFile(); + out.push({ name, mtime: f.lastModified, size: f.size }); + } + return out; + }, + async stat(name) { + try { const f = await (await handle.getFileHandle(name)).getFile(); return { name, mtime: f.lastModified, size: f.size }; } + catch (e) { if (e.name === 'NotFoundError') return null; throw e; } + }, + async read(name) { return (await (await handle.getFileHandle(name)).getFile()).text(); }, + async write(name, text) { + const fh = await handle.getFileHandle(name, { create: true }); + const w = await fh.createWritable(); + await w.write(text); + await w.close(); + const f = await fh.getFile(); + return { mtime: f.lastModified, size: f.size }; + }, + async remove(name) { await handle.removeEntry(name); } + }; +} + +// ----- watching -------------------- + +export function canObserve() { return typeof globalThis.FileSystemObserver === 'function'; } + +// Watch a directory handle. onChanges receives { names: Set, full: boolean }: +// the Markdown files that changed, or full when the browser lost track. Returns +// the observer (call .disconnect()) or null where the API is missing. +export async function observeHandle(handle, onChanges) { + if (!canObserve()) return null; + const observer = new FileSystemObserver((records) => { + const names = new Set(); + let full = false; + for (const r of records) { + if (r.type === 'unknown' || r.type === 'errored') { full = true; continue; } + for (const path of [r.relativePathComponents, r.relativePathMovedFrom]) { + const name = path?.length === 1 ? path[0] : null; + if (name && name.endsWith('.md')) names.add(name); + } + } + if (names.size || full) onChanges({ names, full }); + }); + await observer.observe(handle, { recursive: false }); + return observer; +} + +// Cloud drives land changes in bursts: gather names for a moment, then flush +// once. flush(names | null) runs with null for a full sync. +export function changeCollector(flush, { delay = 1500, setTimer = setTimeout, clearTimer = clearTimeout } = {}) { + let names = new Set(), full = false, timer = null; + return { + add(change) { + if (change.full) full = true; + for (const n of change.names || []) names.add(n); + if (timer) clearTimer(timer); + timer = setTimer(() => { timer = null; const batch = full ? null : names; names = new Set(); full = false; flush(batch); }, delay); + }, + pending: () => timer !== null + }; +} diff --git a/src/lib/zip.js b/src/lib/zip.js new file mode 100644 index 0000000..416f0a2 --- /dev/null +++ b/src/lib/zip.js @@ -0,0 +1,85 @@ +// Minimal ZIP writer (stored, no compression) so an export can be one download. +// Pure: takes [{ name, text }] and returns a Uint8Array. + +const CRC_TABLE = (() => { + const t = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + t[n] = c >>> 0; + } + return t; +})(); + +export function crc32(bytes) { + let c = 0xffffffff; + for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} + +function dosDateTime(d = new Date()) { + const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1); + const date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(); + return { time, date }; +} + +export function makeZip(files, now = new Date()) { + const enc = new TextEncoder(); + const parts = []; + const central = []; + let offset = 0; + const { time, date } = dosDateTime(now); + for (const f of files) { + const name = enc.encode(f.name); + const data = enc.encode(f.text); + const crc = crc32(data); + const local = new DataView(new ArrayBuffer(30)); + local.setUint32(0, 0x04034b50, true); + local.setUint16(4, 20, true); + local.setUint16(6, 0x0800, true); // UTF-8 names + local.setUint16(8, 0, true); + local.setUint16(10, time, true); + local.setUint16(12, date, true); + local.setUint32(14, crc, true); + local.setUint32(18, data.length, true); + local.setUint32(22, data.length, true); + local.setUint16(26, name.length, true); + local.setUint16(28, 0, true); + parts.push(new Uint8Array(local.buffer), name, data); + const cd = new DataView(new ArrayBuffer(46)); + cd.setUint32(0, 0x02014b50, true); + cd.setUint16(4, 20, true); + cd.setUint16(6, 20, true); + cd.setUint16(8, 0x0800, true); + cd.setUint16(10, 0, true); + cd.setUint16(12, time, true); + cd.setUint16(14, date, true); + cd.setUint32(16, crc, true); + cd.setUint32(20, data.length, true); + cd.setUint32(24, data.length, true); + cd.setUint16(28, name.length, true); + cd.setUint16(30, 0, true); + cd.setUint16(32, 0, true); + cd.setUint16(34, 0, true); + cd.setUint16(36, 0, true); + cd.setUint32(38, 0, true); + cd.setUint32(42, offset, true); + central.push(new Uint8Array(cd.buffer), name); + offset += 30 + name.length + data.length; + } + const cdSize = central.reduce((n, p) => n + p.length, 0); + const end = new DataView(new ArrayBuffer(22)); + end.setUint32(0, 0x06054b50, true); + end.setUint16(4, 0, true); + end.setUint16(6, 0, true); + end.setUint16(8, files.length, true); + end.setUint16(10, files.length, true); + end.setUint32(12, cdSize, true); + end.setUint32(16, offset, true); + end.setUint16(20, 0, true); + const all = [...parts, ...central, new Uint8Array(end.buffer)]; + const out = new Uint8Array(all.reduce((n, p) => n + p.length, 0)); + let pos = 0; + for (const p of all) { out.set(p, pos); pos += p.length; } + return out; +} diff --git a/src/note.css b/src/note.css new file mode 100644 index 0000000..531d28c --- /dev/null +++ b/src/note.css @@ -0,0 +1,36 @@ +body { font-size: 14px; } +main { max-width: 860px; margin: 0 auto; padding: 28px 24px 60px; } +.head { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 18px; } +.head > div:first-child { flex: 1; min-width: 0; } +h1 { margin: 0 0 4px; font-size: 20px; letter-spacing: -0.01em; } +#url { display: block; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#meta { font-size: 12px; margin-top: 4px; } +.actions { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; } +.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 20px 22px; margin-bottom: 18px; box-shadow: var(--shadow); } +h2 { margin: 0 0 10px; font-size: 16px; display: flex; align-items: center; gap: 10px; } +.summary { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 13px; margin-bottom: 10px; } +.summary b { font-weight: 650; } +.finding { display: flex; gap: 10px; padding: 8px 10px; border-radius: 8px; margin-bottom: 6px; background: var(--bg); font-size: 13px; } +.finding.ask { background: var(--danger-soft); } .finding.ask .tag { color: var(--danger); } +.finding.edit { background: var(--accent-soft); } .finding.edit .tag { color: var(--accent); } +.finding .tag { flex: 0 0 56px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding-top: 2px; } +.exp { margin-top: 12px; } +.exp table { width: 100%; border-collapse: collapse; font-size: 13px; } +.exp th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); } +.exp td { padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; } +.entries { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; } +.entry { background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; } +.entry .when { font-size: 11px; color: var(--muted); } +.entry .text { white-space: pre-wrap; margin-top: 4px; line-height: 1.45; } +.cap { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 8px; background: var(--bg); } +.cap summary { cursor: pointer; padding: 8px 10px; font-size: 13px; display: flex; gap: 10px; align-items: center; } +.cap summary .muted { font-size: 12px; } +.cap pre { margin: 0; padding: 10px 12px; font-family: var(--mono); font-size: 12px; white-space: pre-wrap; max-height: 320px; overflow: auto; border-top: 1px solid var(--line); } +.diff { font-family: var(--mono); font-size: 12px; } +.diff .add { color: var(--ok); } .diff .del { color: var(--danger); text-decoration: line-through; opacity: 0.8; } +.diff div { padding: 1px 0; white-space: pre-wrap; } +.empty { color: var(--muted); font-size: 13px; } +.sticky-note { padding: 12px 14px; background: var(--sticky-soft); border: 1px solid var(--sticky-edge); border-radius: var(--radius-s); } +.sticky-note .eyebrow { color: var(--sticky-ink); } +.sticky-note blockquote { margin: 8px 0; padding-left: 10px; border-left: 2px solid var(--sticky-edge); color: var(--muted); } +.sticky-note .text { white-space: pre-wrap; overflow-wrap: anywhere; } diff --git a/src/note.html b/src/note.html new file mode 100644 index 0000000..ba956cd --- /dev/null +++ b/src/note.html @@ -0,0 +1,51 @@ + + + + + Note + + + + +
+
+
+

+ +
+
+
All notes
+
+ + +
+

Entries

+
    +
    + + +
    +

    Captures

    +

    What the page looked like each time you saved or opened the popup here. Stored only in this browser.

    +
    +
    +
    + + + diff --git a/src/note.js b/src/note.js new file mode 100644 index 0000000..cc88869 --- /dev/null +++ b/src/note.js @@ -0,0 +1,102 @@ +// Note detail page: entries, insights, captures with diffs. +import { cleanTitle, toMarkdown } from './lib/notes.js'; +import { fmtDate } from './lib/profile.js'; + +const $ = (sel) => document.querySelector(sel); +import { el, fmt, send } from './lib/ui.js'; + +function renderFindings(target, items, cls, tag) { + for (const f of items) target.append(el('div', { class: `finding ${cls}` }, [el('span', { class: 'tag', text: tag }), el('span', { text: f.text })])); +} + +async function init() { + const id = new URLSearchParams(location.search).get('id'); + const { note, insights } = await send('detail', { id }); + document.title = note.title; + $('#title').textContent = note.title; + $('#url').textContent = note.url; + $('#url').href = note.url; + const m = insights?.meta || {}; + const metaBits = [note.host, m.siteName, m.author && `by ${m.author}`, m.published && `published ${m.published.slice(0, 10)}`, note.rule && `identity: ${note.key}`].filter(Boolean); + $('#meta').textContent = metaBits.join(' · '); + $('#copy-md').addEventListener('click', () => navigator.clipboard.writeText(toMarkdown(note))); + + if (insights && (insights.summary || insights.findings?.length || insights.edits?.length)) { + $('#insights').hidden = false; + if (insights.summary) { + const s = insights.summary; + $('#summary').append( + el('span', {}, [el('b', { text: `${s.years} yrs` }), ' experience']), + el('span', {}, [el('b', { text: `${s.employers}` }), ' employers']), + el('span', {}, [el('b', { text: `${Math.round(s.averageTenureMonths / 12 * 10) / 10} yrs` }), ' average tenure']), + el('span', {}, [el('b', { text: `${s.currentRoles}` }), ' current role', s.currentRoles === 1 ? '' : 's']) + ); + $('#copy-card').hidden = false; + $('#copy-card').addEventListener('click', () => navigator.clipboard.writeText(s.card)); + } + if (insights.edits?.length) renderFindings($('#edits'), insights.edits, 'edit', 'edited'); + if (insights.findings?.length) { + renderFindings($('#findings'), insights.findings.filter((f) => f.level === 'ask'), 'ask', 'ask'); + renderFindings($('#findings'), insights.findings.filter((f) => f.level !== 'ask'), 'info', 'info'); + } + if (insights.profile?.experience?.length) { + const table = el('table', {}, [el('thead', {}, [el('tr', {}, ['Role', 'Employer', 'Type', 'From', 'To'].map((h) => el('th', { text: h })))])]); + const body = el('tbody'); + for (const p of insights.profile.experience) body.append(el('tr', {}, [p.title, p.company, p.type || '', fmtDate(p.start), fmtDate(p.end)].map((v) => el('td', { text: v })))); + table.append(body); + $('#experience').append(el('div', { class: 'exp' }, [table])); + } + } + + if (insights?.changed && (insights.changed.addedCount || insights.changed.removedCount || insights.changed.titleChanged)) { + $('#changed').hidden = false; + const c = insights.changed; + const body = $('#changed-body'); + body.append(el('p', { class: 'muted', text: `Compared with the capture from ${fmt(c.since)}: ${c.addedCount} lines added, ${c.removedCount} removed${c.titleChanged ? ', title changed' : ''}.` })); + const diff = el('div', { class: 'diff' }); + c.removed.forEach((l) => diff.append(el('div', { class: 'del', text: `− ${l}` }))); + c.added.forEach((l) => diff.append(el('div', { class: 'add', text: `+ ${l}` }))); + body.append(diff); + } + + const entries = $('#entries'); + [...note.entries].reverse().forEach((e) => entries.append(el('li', { class: 'entry' }, [el('div', { class: 'when', text: fmt(e.at) }), el('div', { class: 'text', text: e.text })]))); + if (note.entries.length === 0) entries.append(el('li', { class: 'empty', text: 'No entries.' })); + + if (note.stickies?.length) { + $('#sticky-section').hidden = false; + note.stickies.forEach((s, i) => $('#stickies').append(el('li', { class: 'sticky-note' }, [ + el('div', { class: 'eyebrow', text: `Sticky ${i + 1}${s.found === false ? ' · location missing' : ''}` }), + el('blockquote', { text: s.anchor?.quote?.exact || s.anchor?.label || 'Page element' }), + el('div', { class: 'text', text: s.text || 'No comment yet' }) + ]))); + } + + const linked = await send('links', { id }); + if (linked.length) { + $('#linked-section').hidden = false; + for (const row of linked) { + $('#linked').append(el('li', {}, [ + el('span', { class: 't' }, [ + row.missing ? el('b', { class: 'muted', text: row.key }) : el('a', { href: `note.html?id=${row.id}`, text: cleanTitle(row.title) || row.url }), + el('span', { text: row.missing ? 'That page\'s note is gone' : `${row.host}${row.entries ? ` · ${row.entries} ${row.entries === 1 ? 'entry' : 'entries'}` : ''}${row.stickies ? ` · ${row.stickies} ${row.stickies === 1 ? 'sticky' : 'stickies'}` : ''}` }) + ]), + row.url && !row.missing ? el('a', { href: row.url, target: '_blank', rel: 'noopener', class: 'muted', text: 'Open page ↗' }) : null + ])); + } + } + + const caps = note.snapshots || []; + $('#capture-count').textContent = String(caps.length); + const holder = $('#captures'); + if (caps.length === 0) holder.append(el('div', { class: 'empty', text: 'No captures yet. Open the popup on the page to take one.' })); + [...caps].reverse().forEach((c, i) => { + const d = el('details', { class: 'cap' }, [ + el('summary', {}, [el('b', { text: fmt(c.at) }), c.seen && c.seen !== c.at ? el('span', { class: 'muted', text: `seen again ${fmt(c.seen)}` }) : null, el('span', { class: 'muted', text: `${c.title || ''} · ${(c.text || '').length} chars${c.structured?.kind ? ` · ${c.structured.kind}` : ''}` })]), + el('pre', { text: c.text || '' }) + ]); + if (i === 0) d.open = true; + holder.append(d); + }); +} +init().catch((e) => { document.body.textContent = e.message; }); diff --git a/src/options.css b/src/options.css new file mode 100644 index 0000000..63bef56 --- /dev/null +++ b/src/options.css @@ -0,0 +1,51 @@ +body { font-size: 14px; } +.page { display: flex; min-height: 100vh; max-width: 1540px; margin: 0 auto; } +.side { width: 190px; flex: 0 0 190px; padding: 28px 18px; position: sticky; top: 0; align-self: flex-start; } +.brand { display: flex; align-items: center; gap: 10px; margin-bottom: 22px; } .brand b { display: block; font-size: 15px; } .brand span { display: block; font-size: 12px; } +nav a { display: block; padding: 8px 12px; border-radius: 8px; color: var(--text); font-weight: 500; } +nav a:hover { background: var(--panel); text-decoration: none; } nav a.on { background: var(--accent-soft); color: var(--accent); } +.save-state { margin-top: 18px; padding: 0 12px; font-size: 12px; min-height: 18px; } +main { flex: 1; min-width: 0; padding: 28px 24px 60px; } +.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 22px 24px; margin-bottom: 20px; box-shadow: var(--shadow); } +h2 { margin: 0 0 10px; font-size: 18px; display: flex; align-items: center; gap: 10px; } +h3 { margin: 26px 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); } +.card > p.muted { margin: 0 0 14px; } +input[type="search"], input[type="url"], textarea { width: 100%; } +.notes { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } +.note { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); } +.note .t { font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.note .u { grid-column: 1; color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.note .p { grid-column: 1; font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; } +.note .m { grid-column: 2; grid-row: 1 / 4; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; font-size: 12px; color: var(--muted); } +.note .m .tools { display: flex; gap: 4px; } .note .m button { padding: 2px 8px; font-size: 12px; } +.row { display: flex; align-items: center; gap: 20px; padding: 14px 0; border-top: 1px solid var(--line); } +.row:first-of-type { border-top: 0; } .row .lbl { flex: 1; } .row .lbl b { display: block; font-weight: 550; } .row .lbl span { display: block; color: var(--muted); font-size: 13px; margin-top: 2px; } +.row-stack { flex-direction: column; align-items: stretch; gap: 12px; } +.seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } +.seg label { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--bg); cursor: pointer; transition: border-color 0.15s var(--ease), background 0.15s var(--ease), box-shadow 0.15s var(--ease); } +.seg label:hover { border-color: var(--accent); } +.seg label:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft); } +.seg label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; } +.seg input { margin: 3px 0 0; accent-color: var(--accent); flex: 0 0 auto; } +.seg span { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.seg b { font-weight: 600; } +.seg small { color: var(--muted); font-size: 12px; line-height: 1.4; } +@media (max-width: 900px) { .seg { grid-template-columns: 1fr; } } +.switch { position: relative; display: inline-block; width: 40px; height: 24px; flex: 0 0 40px; } +.switch input { opacity: 0; width: 0; height: 0; } +.switch i { position: absolute; inset: 0; border-radius: 999px; background: var(--line); transition: background 0.15s; } +.switch i::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: transform 0.15s; } +.switch input:checked + i { background: var(--accent); } .switch input:checked + i::after { transform: translateX(16px); } +.toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px; } +.tester { margin: 12px 0; } #test-result { margin-top: 6px; font-family: var(--mono); font-size: 12px; min-height: 18px; word-break: break-all; } +.help-list { display: flex; flex-direction: column; gap: 10px; } +.help-item { display: flex; gap: 14px; padding: 10px 12px; background: var(--bg); border-radius: 10px; } .help-item b { flex: 0 0 160px; font-weight: 550; } .help-item span { color: var(--muted); } +.empty { padding: 20px; text-align: center; color: var(--muted); } +@media (max-width: 760px) { .page { flex-direction: column; } .side { width: auto; position: static; padding: 16px; } nav { display: flex; flex-wrap: wrap; gap: 4px; } } +.list-head { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } +.list-head input[type="search"] { flex: 1; min-width: 240px; width: auto; } +.toggle { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); white-space: nowrap; } +.notice { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; padding: 12px 14px; border-radius: var(--radius); background: var(--warn-soft); font-size: 13px; } +.notice b { font-weight: 600; } +.notice code { background: var(--panel); padding: 1px 6px; border-radius: 4px; } +.notice button { align-self: flex-start; padding: 2px 8px; font-size: 12px; } diff --git a/src/options.html b/src/options.html new file mode 100644 index 0000000..021868d --- /dev/null +++ b/src/options.html @@ -0,0 +1,107 @@ + + + + + Marginalia (Page Notes) + + + + + +
    + +
    +
    +

    All notes

    +
    + + +
    +
    +
    + +
    +

    Reminders

    +

    Pages you asked to be reminded about, soonest first. Snoozed tabs appear here too.

    +
    +
    + +
    +

    Backup & vault

    +

    Notes are stored in this browser profile. Export them as Markdown files, one per page, with YAML frontmatter that Obsidian understands.

    +
    + + + + +
    +

    Shared stickies

    +

    Someone sent you a .marginalia.txt file? Import it here or paste it in the popup. Their stickies land on your note for that page; if it was encrypted, enter the passphrase they gave you.

    +
    + + + + +
    +

    Vault folder

    +

    Connect a folder, in your Obsidian vault or a synced drive such as iCloud Drive, and your notes live there as Markdown files: written on every save, and read back every few minutes so several browser profiles connected to the same folder share one notebook. Each profile keeps its own schedule; the files are plain Markdown any browser or app can read. The browser asks you to re-allow access once per session.

    + +
    + + + + + +
    +
    + +
    +

    Settings

    +
    The toolbar icon opensHow the note appears when you click the icon or press the shortcut. Every mode can still open the sidebar from its footer.
    +
    + + + +
    +
    +
    Count on the toolbar iconShows how many entries the current page has.
    +
    Open the page when a reminder firesOtherwise you only get a notification.
    +
    Keep captures of pages you noteEach time you save an entry or open the popup on a noted page, store what the page looks like (title, metadata, text) in this browser. That is what powers "changed since your last visit" and profile insights. Never sent anywhere.
    +
    Sync with the vault folderWrite every save to the connected folder and read other profiles' changes back every few minutes.
    +

    Page identity rules

    +

    A note follows its page across URL variants: a pull request and its Files tab, a Jira issue and its board link, a profile and its subpages. Rules are regular expressions with a key template, the same format as Organize Tabs. Test a URL to see the key it gets.

    +
    + +
    +
    + +
    +

    Help

    +
    +
    Add a noteClick the toolbar icon on any page and type. It saves as you type. Each day you write becomes a dated entry; click an earlier entry to edit it in place.
    +
    Capture a quoteSelect text on a page, right-click, "Add selection to this page's note".
    +
    Remind meOne click under the editor: 1h, tonight, tomorrow, next week, or pick a time. The page reopens at that time with a notification.
    +
    Snooze a tabCloses the tab now and reopens it later. Also in the right-click menu and as a keyboard shortcut.
    +
    ExportBackup & vault downloads every note as Markdown, or writes them into an Obsidian vault folder.
    +
    +

    Open source under the MIT license. Source and issues on GitHub.

    +
    +
    +
    + + + diff --git a/src/options.js b/src/options.js new file mode 100644 index 0000000..65b51f6 --- /dev/null +++ b/src/options.js @@ -0,0 +1,225 @@ +import { DEFAULTS, loadSettings, saveSettings } from './lib/settings.js'; +import { canonicalize, compileRules, normalizeUrl } from './lib/url.js'; +import { clearVaultHandle, loadVaultHandle, saveVaultHandle } from './lib/store.js'; +import { createExplorer } from './lib/explorer.js'; +import { canObserve, changeCollector, observeHandle } from './lib/vault.js'; + +const $ = (sel) => document.querySelector(sel); +import { el, fmt, send } from './lib/ui.js'; +const APP_NAME = chrome.runtime.getManifest().name; +let settings = {}; +let explorer; + +function setState(text) { $('#save-state').textContent = text; } + +// ----- notes list -------------------- + +function noteRow(r, { showRemind = false } = {}) { + return el('div', { class: 'note', 'data-id': r.id }, [ + el('a', { class: 't', href: r.url, target: '_blank', rel: 'noopener', text: r.title || r.url }), + el('div', { class: 'u', text: `${r.host}${r.entries ? ` · ${r.entries} entr${r.entries === 1 ? 'y' : 'ies'}` : ' · captured only'}${r.captures ? ` · ${r.captures} capture${r.captures === 1 ? '' : 's'}` : ''}${r.rule ? ` · ${r.rule}` : ''}` }), + el('div', { class: 'p', text: r.preview }), + el('div', { class: 'm' }, [ + el('span', { text: showRemind && r.remind ? `⏰ ${fmt(r.remind)}` : `updated ${fmt(r.updated)}` }), + el('span', { class: 'tools' }, [ + el('button', { text: 'Details', onclick: () => chrome.tabs.create({ url: chrome.runtime.getURL(`src/note.html?id=${r.id}`) }) }), + el('button', { text: 'Open', onclick: () => chrome.tabs.create({ url: r.url }) }), + el('button', { class: 'ghost', text: 'Delete', onclick: async () => { if (confirm(`Delete the note for "${r.title}"?`)) { await send('deleteNote', { id: r.id }); await renderLists(); } } }) + ]) + ]) + ]); +} + +async function renderLists() { + await explorer.reload(); + await renderReminders(); +} + +async function renderReminders() { + const reminders = await send('reminders'); + const rl = $('#reminder-list'); + rl.innerHTML = ''; + if (reminders.length === 0) rl.append(el('div', { class: 'empty', text: 'No reminders set.' })); + reminders.forEach((r) => rl.append(noteRow(r, { showRemind: true }))); +} + +// ----- backup -------------------- + +async function exportAll() { + const { count, files, base64 } = await send('export'); + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + const blob = new Blob([bytes], { type: 'application/zip' }); + const a = el('a', { href: URL.createObjectURL(blob), download: `marginalia-${new Date().toISOString().slice(0, 10)}.zip` }); + document.body.append(a); a.click(); a.remove(); + $('#backup-status').textContent = `Exported ${count} note${count === 1 ? '' : 's'} and ${files - count} capture${files - count === 1 ? '' : 's'}.`; +} + +async function importFiles(fileList) { + const files = []; + for (const f of fileList) files.push({ name: f.name, text: await f.text() }); + const r = await send('import', { files }); + $('#backup-status').textContent = `Imported ${r.imported}, merged ${r.merged}${r.errors.length ? `, ${r.errors.length} failed: ${r.errors[0]}` : ''}.`; + await renderLists(); +} + +// ----- vault -------------------- + +const FS_ACCESS = typeof window.showDirectoryPicker === 'function'; + +async function renderVault() { + const handle = await loadVaultHandle(); + const status = $('#vault-status'); + if (!FS_ACCESS) { + $('#vault-connect').disabled = true; + $('#vault-unsupported').hidden = false; + $('#vault-disconnect').hidden = true; + $('#vault-write').hidden = true; + $('#vault-reconnect').hidden = true; + status.textContent = ''; + return; + } + $('#vault-connect').hidden = !!handle; + $('#vault-disconnect').hidden = !handle; + $('#vault-write').hidden = !handle; + $('#vault-reconnect').hidden = true; + if (!handle) { status.textContent = 'Not connected.'; return; } + const perm = await handle.queryPermission({ mode: 'readwrite' }); + const st = await send('vaultStatus').catch(() => ({})); + const how = canObserve() ? 'Changes from other profiles are picked up as they land, with a check every 5 minutes as a safety net.' : 'Checks the folder every 5 minutes.'; + const last = st.at ? `${how} Last sync ${fmt(st.at)}${st.report ? ` · ${st.report.scanned} files, ${st.report.imported + st.report.updated} read in, ${st.report.written} written${st.report.errors?.length ? `, ${st.report.errors.length} skipped` : ''}` : ''}.` : 'Not synced yet.'; + if (perm === 'granted') status.textContent = `Connected to "${handle.name}". ${last}`; + else { status.textContent = `Connected to "${handle.name}", access needs re-allowing this session. ${last}`; $('#vault-reconnect').hidden = false; } +} + +// Settings is open while you look at the vault: watch the folder from here too. +let pageObserver = null; +async function watchVaultFromPage() { + if (pageObserver || !canObserve()) return; + try { + const handle = await loadVaultHandle(); + if (!handle || (await handle.queryPermission({ mode: 'readwrite' })) !== 'granted') return; + const changes = changeCollector((names) => send('syncVault', { only: names ? [...names] : undefined }).catch(() => {})); + pageObserver = await observeHandle(handle, (change) => changes.add(change)); + } catch (e) { /* the alarm still runs */ } +} + +async function syncNow() { + watchVaultFromPage(); + $('#vault-status').textContent = 'Syncing…'; + const r = await send('syncVault'); + await renderVault(); + if (r.status !== 'ok') $('#vault-status').textContent += ` Could not sync (${r.status}${r.error ? `: ${r.error}` : ''}).`; +} + +async function importShared(file) { + const status = $('#share-status'); + try { + const r = await send('shareImport', { text: await file.text(), passphrase: $('#share-pass').value }); + status.textContent = `Added ${r.addedStickies} stick${r.addedStickies === 1 ? 'y' : 'ies'}${r.addedEntries ? ` and ${r.addedEntries} ${r.addedEntries === 1 ? 'entry' : 'entries'}` : ''} to “${r.title}”.`; + await renderLists(); + } catch (e) { status.textContent = e.message; } +} + +async function connectVault() { + try { + const handle = await window.showDirectoryPicker({ mode: 'readwrite' }); + await saveVaultHandle(handle); + await renderVault(); + await syncNow(); + } catch (e) { + if (e.name !== 'AbortError') $('#vault-status').textContent = e.message; + } +} + +async function reconnectVault() { + const handle = await loadVaultHandle(); + if (!handle) return; + await handle.requestPermission({ mode: 'readwrite' }); + await syncNow(); +} + +// ----- settings -------------------- + +const BOOLS = ['showBadge', 'openTabOnReminder', 'snapshotOnSave', 'writeToVault']; + +function renderSettings() { + BOOLS.forEach((k) => { $(`#${k}`).checked = !!settings[k]; }); + document.querySelectorAll('input[name="launch"]').forEach((r) => { r.checked = r.value === settings.launch; }); + $('#rules').value = JSON.stringify(settings.rules.map(({ name, match, key, enabled, flags }) => ({ name, match, key, enabled: enabled !== false, ...(flags !== undefined ? { flags } : {}) })), null, 2); +} + +async function save() { + const where = await saveSettings(settings); + setState(where === 'sync' ? 'Saved and synced' : 'Saved locally'); + await send('settingsChanged'); +} + +function runTester() { + const url = $('#test-url').value.trim(); + const out = $('#test-result'); + if (!url) { out.textContent = ''; return; } + const { compiled } = compileRules(settings.rules); + const { key, rule } = canonicalize(url, compiled, settings.normalization); + out.textContent = rule ? `${rule} → ${key}` : `No rule matched. Normalized → ${normalizeUrl(url, settings.normalization)}`; +} + +function applyRules() { + const err = $('#rules-error'); + try { + const rules = JSON.parse($('#rules').value); + if (!Array.isArray(rules)) throw new Error('Expected an array'); + rules.forEach((r, i) => { if (typeof r.match !== 'string' || typeof r.key !== 'string') throw new Error(`Rule ${i + 1} needs string "match" and "key"`); }); + const { errors } = compileRules(rules); + if (errors.length) throw new Error(`${errors[0].rule.name || 'A rule'}: ${errors[0].error}`); + settings.rules = rules; + err.hidden = true; + save(); + runTester(); + } catch (e) { + err.textContent = e.message; + err.hidden = false; + } +} + +// ----- init -------------------- + +async function init() { + $('#app-name').textContent = APP_NAME; + document.title = APP_NAME; + $('#version').textContent = `v${chrome.runtime.getManifest().version}`; + settings = (await loadSettings()).settings; + renderSettings(); + explorer = createExplorer($('#note-list'), { search: $('#search'), includeCaptures: $('#include-captures'), onCount: (n) => { $('#note-count').textContent = String(n); }, onChange: renderReminders }); + await renderLists(); + await renderVault(); + watchVaultFromPage(); + + let listTimer; + chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes['notes:index']) { clearTimeout(listTimer); listTimer = setTimeout(renderLists, 150); } + }); + $('#export').addEventListener('click', () => exportAll().catch((e) => { $('#backup-status').textContent = e.message; })); + $('#import').addEventListener('click', () => $('#import-file').click()); + $('#import-file').addEventListener('change', (e) => importFiles(e.target.files).catch((err) => { $('#backup-status').textContent = err.message; }).finally(() => { e.target.value = ''; })); + $('#vault-connect').addEventListener('click', connectVault); + $('#copy-flag').addEventListener('click', () => { navigator.clipboard.writeText('brave://flags/#file-system-access-api'); $('#copy-flag').textContent = 'Copied'; }); + $('#vault-reconnect').addEventListener('click', reconnectVault); + $('#vault-write').addEventListener('click', () => syncNow().catch((e) => { $('#vault-status').textContent = e.message; })); + $('#share-import').addEventListener('click', () => $('#share-file').click()); + $('#share-file').addEventListener('change', (e) => { if (e.target.files[0]) importShared(e.target.files[0]).finally(() => { e.target.value = ''; }); }); + chrome.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes['vault:status']) renderVault(); }); + $('#vault-disconnect').addEventListener('click', async () => { await clearVaultHandle(); await renderVault(); }); + BOOLS.forEach((k) => $(`#${k}`).addEventListener('change', (e) => { settings[k] = e.target.checked; save(); })); + document.querySelectorAll('input[name="launch"]').forEach((r) => r.addEventListener('change', (e) => { if (e.target.checked) { settings.launch = e.target.value; save(); } })); + $('#test-url').addEventListener('input', runTester); + $('#rules-apply').addEventListener('click', applyRules); + $('#rules-reset').addEventListener('click', () => { settings.rules = DEFAULTS.rules.map((r) => ({ ...r })); renderSettings(); save(); runTester(); }); + + const links = [...document.querySelectorAll('nav a')]; + const observer = new IntersectionObserver((entries) => { + entries.forEach((en) => { if (en.isIntersecting) links.forEach((a) => a.classList.toggle('on', a.getAttribute('href') === `#${en.target.id}`)); }); + }, { rootMargin: '-40% 0px -55% 0px' }); + links.forEach((a) => observer.observe($(a.getAttribute('href')))); + if (location.hash) $(location.hash)?.scrollIntoView(); +} +init(); diff --git a/src/panel.html b/src/panel.html new file mode 100644 index 0000000..3cfbf0c --- /dev/null +++ b/src/panel.html @@ -0,0 +1,83 @@ + + + + + Marginalia (Page Notes) + + + + + +
    + + +
    +

    +
    +
    + + +
    + +
    + + + +
    +
    + +
    + Remind + + + + + + +
    + +
    + + + +
      +
      + + + + + + + + diff --git a/src/popup.css b/src/popup.css new file mode 100644 index 0000000..13c2357 --- /dev/null +++ b/src/popup.css @@ -0,0 +1,145 @@ +body { width: 368px; } + +/* header: page identity */ +.head { display: flex; align-items: center; gap: 10px; padding: 12px 14px 11px; background: var(--panel); border-bottom: 1px solid var(--line); } +.head .fav { width: 22px; height: 22px; border-radius: 6px; background: var(--panel-2); flex: 0 0 22px; object-fit: contain; } +.head-text { flex: 1; min-width: 0; } +.head h1 { margin: 0; font-size: var(--fs-m); font-weight: 650; letter-spacing: -0.005em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#page-url { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +main { padding: 12px 12px 6px; max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; } + +/* editor: the note for today */ +#compose { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-s); padding: 10px 12px 10px; transition: box-shadow 0.2s var(--ease), border-color 0.2s var(--ease); } +#compose:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-s); } +.editor-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: var(--fs-xs); min-height: 14px; margin-bottom: 4px; } +#today-label { color: var(--muted); font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; font-size: 10.5px; } +.status { color: var(--muted); font-size: var(--fs-xs); transition: opacity 0.4s var(--ease); } +.status.saved { color: var(--ok); } +.status.error { color: var(--danger-text); } +#compose textarea { width: 100%; resize: none; min-height: 68px; border: 0; padding: 2px 0; background: transparent; box-shadow: none; font-size: var(--fs-m); line-height: 1.5; } +#compose textarea:focus { box-shadow: none; outline: none; } +#compose textarea::placeholder { color: var(--muted); opacity: 0.9; } +.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 3px; margin-top: 6px; padding-top: 8px; border-top: 1px solid var(--line); } +.chip-label { font-size: var(--fs-xs); color: var(--muted); margin-right: 2px; } +.chip { font-size: var(--fs-xs); font-weight: 500; padding: 2px 8px; border-radius: 999px; border: 1px solid transparent; background: var(--panel-2); color: var(--text-2); line-height: 1.35; cursor: pointer; } +.chip:hover { background: var(--accent-soft); color: var(--accent-text); } +.chip.pick { position: relative; overflow: hidden; } +.chip.pick input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; } +.chip.snooze { margin-left: auto; color: var(--accent-text); background: var(--accent-soft); } +.chip.snooze:hover { background: var(--accent); color: #fff; } +.reminder { margin-top: 8px; padding: 6px 10px; border-radius: var(--radius-s); background: var(--accent-soft); color: var(--accent-text); font-size: var(--fs-s); font-weight: 500; display: flex; align-items: center; gap: 8px; } +.reminder button { margin-left: auto; padding: 0 6px; font-size: var(--fs-xs); color: var(--accent-text); } + +/* insights: what the captures say */ +.insights { display: flex; flex-direction: column; gap: 6px; } +.insights .ins { padding: 7px 10px; border-radius: var(--radius-s); font-size: var(--fs-s); line-height: 1.4; background: var(--panel); border: 1px solid var(--line); color: var(--text-2); } +.insights .ins.ask { background: var(--warn-soft); border-color: transparent; color: var(--text); } +.insights .ins.edit { background: var(--accent-soft); border-color: transparent; color: var(--accent-text); } +.insights .ins.summary { display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--muted); } +.insights .ins.summary b { color: var(--text); font-weight: 650; } +.insights .ins .tag { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 6px; } +.insights .ins.ask .tag { color: var(--warn); } +.insights .ins.edit .tag { color: var(--accent-text); } +.insights .more { font-size: var(--fs-xs); color: var(--muted); padding: 0 2px; } +.insights .more a { cursor: pointer; } + +/* earlier entries: a dated timeline */ +.entries { list-style: none; margin: 0; padding: 0 0 0 14px; position: relative; display: flex; flex-direction: column; gap: 10px; } +.entries::before { content: ''; position: absolute; left: 4px; top: 6px; bottom: 6px; width: 1px; background: var(--line-2); } +.entry { position: relative; } +.entry::before { content: ''; position: absolute; left: -13px; top: 6px; width: 7px; height: 7px; border-radius: 50%; background: var(--panel); border: 1.5px solid var(--line-2); } +.entry:hover::before { border-color: var(--accent); } +.entry .when { font-size: var(--fs-xs); color: var(--muted); display: flex; align-items: center; gap: 8px; min-height: 18px; } +.entry .when .tools { margin-left: auto; display: none; gap: 4px; } +.entry:hover .when .tools, .entry.editing .when .tools { display: inline-flex; } +.entry .tools button { padding: 0 6px; font-size: var(--fs-xs); } +.entry .text { white-space: pre-wrap; word-break: break-word; margin-top: 2px; line-height: 1.5; cursor: text; border-radius: var(--radius-s); padding: 4px 6px; margin-left: -6px; color: var(--text); transition: background 0.15s var(--ease); } +.entry .text:hover { background: var(--panel-2); } +.entry .text[contenteditable="true"] { background: var(--panel); box-shadow: 0 0 0 2px var(--accent); outline: none; } +.entry .text .q { border-left: 2px solid var(--line-2); margin: 2px 0; padding-left: 8px; color: var(--muted); } +.entries .hint { color: var(--muted); font-size: var(--fs-xs); padding: 2px 0 0; } +.entries .hint::before { display: none; } +.empty { padding: 28px 12px; text-align: center; color: var(--muted); } + +.foot { display: flex; align-items: center; gap: 14px; padding: 8px 14px; border-top: 1px solid var(--line); background: var(--panel); font-size: var(--fs-s); } +.foot .spacer { flex: 1; } +.toast { position: absolute; left: 12px; right: 12px; bottom: 46px; padding: 9px 12px; border-radius: 10px; background: var(--text); color: var(--bg); box-shadow: var(--shadow-l); z-index: 30; font-weight: 500; font-size: var(--fs-s); } +.toast.error { background: var(--danger); color: #fff; } + +/* side panel: fluid width, full height, editor pinned at the top */ +body.panel { width: auto; min-width: 280px; height: 100vh; display: flex; flex-direction: column; } +body.panel main { flex: 1; max-height: none; } +body.panel .foot { flex-wrap: wrap; row-gap: 4px; } +body.panel #capture-hint { flex-basis: 100%; font-size: var(--fs-xs); } + +/* linked pages: connections across sites */ +.linked { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; } +.linked-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; } +.linked-head .spacer { flex: 1; } +#link-picker { margin: 6px 0 8px; } +#linked-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } +#linked-list li { display: grid; grid-template-columns: minmax(60px, auto) minmax(0, 1fr) auto auto; gap: 2px 8px; align-items: center; padding: 6px 0; border-top: 1px solid var(--line); cursor: pointer; } +#linked-list li:first-child { border-top: 0; } +#linked-list .host { font-size: var(--fs-xs); color: var(--accent-text); background: var(--accent-soft); padding: 1px 7px; border-radius: 999px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; justify-self: start; } +#linked-list .title { font-size: var(--fs-s); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#linked-list li:hover .title { color: var(--accent-text); } +#linked-list .go, #linked-list .x { opacity: 0; font-size: var(--fs-xs); color: var(--muted); padding: 0 4px; border: 0; background: transparent; } +#linked-list li:hover .go, #linked-list li:hover .x, #linked-list li:focus-within .go, #linked-list li:focus-within .x { opacity: 1; } +#linked-list .x:hover { color: var(--danger); } +#linked-list li.missing { cursor: default; } #linked-list li.missing .title { color: var(--muted); text-decoration: line-through; } +#linked-list li.empty-row { display: block; font-size: var(--fs-s); line-height: 1.6; color: var(--muted); padding: 6px 0 2px; cursor: default; } + +/* vault: once per session the browser wants a click */ +.vault-hint { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--radius-s); background: var(--warn-soft); color: var(--text); font-size: var(--fs-s); } +.vault-hint span { flex: 1; } + +/* sharing stickies as a flat file */ +.share-panel { display: grid; gap: 8px; margin: 6px 0 10px; padding: 10px; border: 1px solid var(--sticky-edge); border-radius: var(--radius-s); background: var(--panel); } +.share-panel .share-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-xs); color: var(--muted); } +.share-panel .share-row .spacer { flex: 1; } +.share-panel input[type="password"], .share-panel input[type="text"] { flex: 1; min-width: 120px; font-size: var(--fs-s); padding: 5px 8px; } +.share-panel textarea { width: 100%; min-height: 84px; font-family: var(--mono); font-size: 10.5px; line-height: 1.4; resize: vertical; } +.share-panel label { display: flex; align-items: center; gap: 6px; } +.share-panel .share-status { font-size: var(--fs-xs); color: var(--muted); min-height: 16px; } +.share-panel .share-status.ok { color: var(--ok); } +.share-panel .share-status.error { color: var(--danger-text); } + +/* stickies: notes pinned to parts of the page */ +.stickies { background: var(--sticky-soft); border: 1px solid var(--sticky-edge); border-radius: var(--radius); padding: 10px 12px; } +.stickies-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; } +.stickies-head .eyebrow { color: var(--sticky-ink); } +.stickies-head .spacer { flex: 1; } +button.tiny { font-size: var(--fs-xs); padding: 2px 8px; border-radius: 999px; } +#sticky-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } +#sticky-list li { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 4px 8px; align-items: start; padding: 6px 0; border-top: 1px solid var(--sticky-edge); cursor: pointer; } +#sticky-list li:first-child { border-top: 0; } +#sticky-list .n { width: 18px; height: 20px; border-radius: 3px 3px 7px 3px; background: #f4cd6b; color: #50390c; font-size: 10px; font-weight: 700; line-height: 20px; text-align: center; margin-top: 1px; } +#sticky-list li.orphan .n { background: var(--warn); } +#sticky-list .q { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#sticky-list .t { font-size: var(--fs-s); color: var(--text); white-space: pre-wrap; } +#sticky-list .t.empty { color: var(--muted); font-style: italic; } +#sticky-list .x { opacity: 0; font-size: var(--fs-xs); color: var(--muted); padding: 0 4px; border: 0; background: transparent; } +#sticky-list li:hover .x, #sticky-list li:focus-within .x { opacity: 1; } +#sticky-list .x:hover { color: var(--danger); } +#sticky-list li.empty-row { display: block; font-size: var(--fs-s); line-height: 1.6; color: var(--muted); padding: 8px 0 4px; cursor: default; } +.always { display: flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line); } +.always input { margin: 0; } +.back-button, .close-button { border: 0; background: transparent; color: var(--muted); padding: 4px 7px; font-size: 20px; line-height: 1; flex: 0 0 auto; border-radius: var(--radius-s); } +.back-button:hover, .close-button:hover { background: var(--panel-2); color: var(--text); } +.close-button { font-size: 14px; padding: 6px 8px; } +.library-title { flex: 1; min-width: 0; } +.foot .return { font-weight: 600; } +/* embedded in the floating widget: the frame is the viewport, the layer's bar carries the title */ +body.embed { min-width: 0; } +body.embed .head .close-button, body.embed .library-head .close-button { display: none; } +body.panel main { min-height: 0; } +#library { flex: 1; min-height: 0; overflow: auto; padding: 14px; } +.library-head { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; position: sticky; top: -14px; z-index: 2; background: var(--bg); padding: 14px 0 8px; margin-top: -14px; } +.library-head h2 { font-size: 18px; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.library-head .muted { font-size: var(--fs-xs); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +body.browsing #capture-hint, body.browsing #open-detail { display: none !important; } +.saved-here { display: flex; align-items: center; gap: 8px; color: var(--accent-text); background: var(--accent-soft); border-radius: var(--radius-s); padding: 8px 10px; font-size: var(--fs-s); } +.saved-here::before { content: ''; width: 7px; height: 7px; flex-shrink: 0; border-radius: 50%; background: var(--danger); } +.saved-here.arrived { animation: saved-arrival .8s ease-out; } +@keyframes saved-arrival { 0% { box-shadow: 0 0 0 0 var(--accent); } 100% { box-shadow: 0 0 0 7px transparent; } } diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 0000000..a3a4c1b --- /dev/null +++ b/src/popup.html @@ -0,0 +1,74 @@ + + + + + Marginalia (Page Notes) + + + + +
      + +
      +

      +
      +
      + +
      + +
      + + + +
      +
      + +
      + Remind + + + + + + +
      + +
      + + + +
        +
        + + + + + + diff --git a/src/popup.js b/src/popup.js new file mode 100644 index 0000000..8334269 --- /dev/null +++ b/src/popup.js @@ -0,0 +1,613 @@ +// The popup shows the note for the current page and sends every change to the +// service worker. The same page runs three ways: as the popup (bound to the +// tab it opened on), as the side panel (follows its window's active tab) and +// embedded in the floating widget on a page (bound to that tab, closes by +// asking the layer around it). + +const $ = (sel) => document.querySelector(sel); +import { el, fmt, send } from './lib/ui.js'; +import { createExplorer } from './lib/explorer.js'; +import { createPagePicker } from './lib/picker.js'; +import { cleanTitle } from './lib/notes.js'; +import { loadVaultHandle } from './lib/store.js'; +import { canObserve, changeCollector, observeHandle } from './lib/vault.js'; +const APP_NAME = chrome.runtime.getManifest().name; +const SHORT_NAME = chrome.runtime.getManifest().short_name || APP_NAME; +const EMBED = new URLSearchParams(location.search).has('embed'); +if (EMBED) document.body.classList.add('embed'); +const PANEL = document.body.classList.contains('panel'); +let ownTabId = null; // the tab the embedded widget sits in +let current = null; // { tab, ident, note, insights, today } +let draftTimer = null; +let lastSentText = null; +let draftPromise = null; +let refreshVersion = 0; +let library = null; +let libraryVisible = false; +let noticedPage = null; + +// The library sits above the page in the hierarchy: Sites > Pages > this page. +// Going back from the page lands on its site's page list; the arrow inside +// the library climbs one level and disappears at the top. Returning to the +// current page is a separate, forward action in the footer. +async function showLibrary(show = true, { root = false } = {}) { + if (!PANEL || !await flushDraft()) return; + libraryVisible = show; + document.body.classList.toggle('browsing', show); + $('#open-all').hidden = show; + $('#return-to-page').hidden = !show; + $('.head').hidden = show; + $('main').hidden = show; + $('#library').hidden = !show; + if (show) { + library ||= createExplorer($('#library-explorer'), { onNavigate: (depth, where) => { + // the library header names the level: all notes, a site, a page + $('#library-up').hidden = depth === 0; + const h2 = $('#library-title'), sub = $('#library-sub'); + const pages = (n) => `${n} saved page${n === 1 ? '' : 's'}`; + if (depth === 0) { h2.textContent = 'All notes'; sub.textContent = where.sites ? `${pages(where.pages)} across ${where.sites} site${where.sites === 1 ? '' : 's'}` : 'Your pages, together.'; } + else if (depth === 1) { h2.textContent = where.host || 'All pages'; sub.textContent = where.host ? pages(where.pages) : `${pages(where.pages)}, listed by site`; } + else { h2.textContent = where.title || 'Saved page'; sub.textContent = where.site || where.host || 'Saved page'; } + } }); + await library.reload(); + const host = current?.note?.host || (current?.tab?.url && /^https?:/.test(current.tab.url) ? new URL(current.tab.url).hostname.replace(/^www\./, '') : ''); + library.focus(root ? {} : { host, id: current?.ident?.id }); + $('#library-explorer input[type="search"]').focus({ preventScroll: true }); + } else await refresh({ capture: false }); +} + +// Close this view: the popup window, the side panel, or the widget around us. +function dismiss() { + if (EMBED) window.parent.postMessage({ type: 'pn:widget-close' }, '*'); + else window.close(); +} + +let toastTimer; +function toast(msg, error = false) { + const t = $('#toast'); + t.textContent = msg; + t.className = `toast${error ? ' error' : ''}`; + t.hidden = false; + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { t.hidden = true; }, 2500); +} + +function setStatus(text, cls = '') { + const st = $('#status'); + st.textContent = text; + st.className = `status ${cls}`; +} + +function renderText(text) { + const wrap = el('div', { class: 'text' }); + let quote = null; + for (const line of text.split('\n')) { + if (line.startsWith('> ')) { + if (!quote) { quote = el('div', { class: 'q' }); wrap.append(quote); } + quote.append(el('div', { text: line.slice(2) })); + } else { + quote = null; + wrap.append(el('div', { text: line || ' ' })); + } + } + return wrap; +} + +function renderInsights(ins) { + const box = $('#insights'); + box.innerHTML = ''; + if (!ins) { box.hidden = true; return; } + const rows = []; + for (const e of (ins.edits || []).slice(0, 3)) rows.push(el('div', { class: 'ins edit' }, [el('span', { class: 'tag', text: 'edited' }), e.text])); + for (const f of (ins.findings || []).filter((x) => x.level === 'ask').slice(0, 3)) rows.push(el('div', { class: 'ins ask' }, [el('span', { class: 'tag', text: 'ask' }), f.text])); + if (ins.summary) { + const s = ins.summary; + rows.push(el('div', { class: 'ins summary' }, [ + el('span', {}, [el('b', { text: `${s.years} yrs` }), ' exp']), + el('span', {}, [el('b', { text: String(s.employers) }), ' employers']), + el('span', {}, [el('b', { text: `${Math.round(s.averageTenureMonths / 12 * 10) / 10} yrs` }), ' avg tenure']), + el('a', { class: 'ghost', text: 'copy card', title: s.card, onclick: () => { navigator.clipboard.writeText(s.card); toast('Card line copied'); } }) + ])); + } else if (ins.changed && (ins.changed.addedCount || ins.changed.removedCount || ins.changed.titleChanged)) { + rows.push(el('div', { class: 'ins edit' }, [el('span', { class: 'tag', text: 'changed' }), `Changed since ${fmt(ins.changed.since)}: ${ins.changed.addedCount} lines added, ${ins.changed.removedCount} removed.`])); + } + const shown = rows.filter((r) => !r.classList.contains('summary')).length; + const extra = (ins.findings?.length || 0) + (ins.edits?.length || 0) - shown; + if (rows.length > 0 || ins.captures > 1) { + rows.push(el('div', { class: 'more' }, [el('a', { text: `${extra > 0 ? `${extra} more · ` : ''}${ins.captures} capture${ins.captures === 1 ? '' : 's'} · details`, onclick: openDetail })])); + } + rows.forEach((r) => box.append(r)); + box.hidden = rows.length === 0; +} + +function renderStickies() { + const { tab, note, alwaysShow } = current || {}; + const box = $('#stickies'); + const web = tab && /^https?:/.test(tab.url || ''); + box.hidden = !web; + if (!web) return; + const list = $('#sticky-list'); + list.innerHTML = ''; + const stickies = note?.stickies || []; + $('#toggle-stickies').hidden = stickies.length === 0; + $('#toggle-stickies').textContent = current.stickiesVisible === false ? 'Show all' : 'Hide all'; + $('#toggle-stickies').setAttribute('aria-pressed', String(current.stickiesVisible !== false)); + $('#toggle-stickies').title = 'Show or hide stickies on all pages'; + $('#copy-feedback').hidden = stickies.length === 0; + $('#share-stickies').hidden = stickies.length === 0; + if (stickies.length === 0) { + list.append(el('li', { class: 'empty-row', text: 'Pin a comment to any part of this page.' })); + } + stickies.forEach((st, i) => { + const quote = st.anchor?.quote?.exact ? `“${st.anchor.quote.exact.slice(0, 70)}${st.anchor.quote.exact.length > 70 ? '…' : ''}”` : (st.anchor?.label || st.anchor?.tag || 'element'); + const li = el('li', { class: st.found === false ? 'orphan' : '', title: st.found === false ? 'Could not find this spot on the page as it is now' : 'Show on the page' }, [ + el('span', { class: 'n', text: String(i + 1) }), + el('span', {}, [el('div', { class: 'q', text: quote }), el('div', { class: `t${st.text ? '' : ' empty'}`, text: st.text || 'No comment yet' })]), + el('button', { class: 'x', text: '✕', title: 'Remove', onclick: async (e) => { e.stopPropagation(); await send('stickyRemove', { url: tab.url, id: st.id }); await refresh({ keepEditor: true }); } }) + ]); + li.addEventListener('click', async () => { try { await send('annotate', { tabId: tab.id, scrollTo: st.id }); if (!PANEL || EMBED) dismiss(); } catch (e) { toast(e.message, true); } }); + list.append(li); + }); + const always = $('#always-show'); + always.hidden = !alwaysShow?.origin || stickies.length === 0; + if (alwaysShow?.origin) { + $('#always-origin').textContent = alwaysShow.origin.replace(/^https?:\/\//, ''); + $('#always-show-toggle').checked = !!alwaysShow.enabled; + } +} + +// Linked pages: connections the user drew between pages, across sites. +let picker = null; +function renderLinked() { + const { tab, linked = [], note } = current || {}; + const box = $('#linked'); + const web = tab && /^(https?|file):/.test(tab.url || ''); + box.hidden = !web; + if (!web) return; + const list = $('#linked-list'); + list.innerHTML = ''; + if (!linked.length && !picker) list.append(el('li', { class: 'empty-row', text: 'Connect this page to another saved page, on any site: your GitHub profile to your LinkedIn, a ticket to its pull request.' })); + for (const row of linked) { + const li = el('li', { class: row.missing ? 'missing' : '', title: row.missing ? 'That page\'s note is gone' : (PANEL ? 'Show its notes' : 'Open the page') }, [ + el('span', { class: 'host', text: row.host || '·' }), + el('span', { class: 'title', text: cleanTitle(row.title) || row.url || row.key }), + el('button', { class: 'go', text: '↗', title: 'Open the page', 'aria-label': 'Open the page', hidden: row.missing || !row.url, onclick: async (e) => { e.stopPropagation(); try { await send('openNote', { id: row.id, page: true }); if (!PANEL || EMBED) dismiss(); } catch (err) { toast(err.message, true); } } }), + el('button', { class: 'x', text: '✕', title: 'Unlink', 'aria-label': 'Unlink', onclick: async (e) => { e.stopPropagation(); try { const r = await send('unlink', { id: note.id, otherId: row.id }); current = { ...current, note: r.note, linked: r.linked }; renderLinked(); } catch (err) { toast(err.message, true); } } }) + ]); + if (!row.missing) li.addEventListener('click', async () => { + try { + if (PANEL) { await showLibrary(true); library.show(row.id); } + else await send('openNote', { id: row.id }); + } catch (err) { toast(err.message, true); } + }); + list.append(li); + } +} +function openLinkPicker() { + if (picker) { picker.focus(); return; } + const holder = $('#link-picker'); + const exclude = new Set([current?.ident?.id, ...(current?.linked || []).map((r) => r.id)].filter(Boolean)); + picker = createPagePicker({ + exclude, + placeholder: 'Type to find a page to link…', + onPick: async (row) => { + closeLinkPicker(); + try { + if (!await flushDraft()) return; + const r = await send('link', { ...(current.note ? { id: current.note.id } : { tabId: current.tab.id, url: current.tab.url, title: current.tab.title }), otherId: row.id }); + current = { ...current, note: r.note, linked: r.linked }; + toast(`Linked to ${cleanTitle(row.title)}`); + await refresh({ keepEditor: true, capture: false }); + } catch (err) { toast(err.message, true); } + }, + onClose: closeLinkPicker + }); + holder.replaceChildren(picker.root); + holder.hidden = false; + $('#add-link').setAttribute('aria-expanded', 'true'); + renderLinked(); + picker.focus(); +} +function closeLinkPicker() { + if (!picker) return; + picker = null; + $('#link-picker').hidden = true; + $('#link-picker').replaceChildren(); + $('#add-link').setAttribute('aria-expanded', 'false'); + renderLinked(); +} + +// ----- sharing stickies as a flat file -------------------- +let sharePanel = null; // 'export' | 'import' | null +function closeSharePanel() { + sharePanel = null; + const box = $('#share-panel'); + box.hidden = true; box.replaceChildren(); + $('#share-stickies').setAttribute('aria-expanded', 'false'); + $('#import-stickies').setAttribute('aria-expanded', 'false'); +} +async function openSharePanel(mode) { + if (sharePanel === mode) { closeSharePanel(); return; } + closeSharePanel(); + sharePanel = mode; + const box = $('#share-panel'); + box.hidden = false; + $(mode === 'export' ? '#share-stickies' : '#import-stickies').setAttribute('aria-expanded', 'true'); + const status = el('div', { class: 'share-status', role: 'status' }); + const pass = el('input', { type: 'password', placeholder: mode === 'export' ? 'Passphrase (optional, encrypts the file)' : 'Passphrase, if it was encrypted', autocomplete: 'off', 'aria-label': 'Passphrase' }); + if (mode === 'export') { + const includeEntries = el('input', { type: 'checkbox' }); + const out = el('textarea', { readonly: true, 'aria-label': 'Shared stickies text', spellcheck: 'false' }); + let result = null; + const build = async () => { + try { + result = await send('shareExport', { id: current.note.id, passphrase: pass.value, includeEntries: includeEntries.checked }); + out.value = result.text; + status.className = 'share-status'; + status.textContent = `${result.stickies} stick${result.stickies === 1 ? 'y' : 'ies'}${result.entries ? ` and ${result.entries} ${result.entries === 1 ? 'entry' : 'entries'}` : ''}, ${Math.round(result.text.length / 1024 * 10) / 10} KB${pass.value ? ', encrypted' : ', compressed only (anyone with the extension can read it)'}.`; + } catch (e) { status.className = 'share-status error'; status.textContent = e.message; } + }; + let passTimer; pass.addEventListener('input', () => { clearTimeout(passTimer); passTimer = setTimeout(build, 350); }); + includeEntries.addEventListener('change', build); + box.append( + el('div', { class: 'share-row' }, [el('b', { text: 'Share these stickies' }), el('span', { class: 'spacer' }), el('label', {}, [includeEntries, 'Include my notes too'])]), + el('div', { class: 'share-row' }, [pass]), + out, + el('div', { class: 'share-row' }, [ + el('button', { class: 'tiny primary', text: 'Copy', onclick: async () => { if (!result) return; await navigator.clipboard.writeText(result.text); toast('Copied. Paste it into an email or chat.'); } }), + el('button', { class: 'tiny', text: 'Download .txt', onclick: () => { if (!result) return; const a = el('a', { href: URL.createObjectURL(new Blob([result.text], { type: 'text/plain' })), download: result.fileName }); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } }), + el('span', { class: 'spacer' }), + el('button', { class: 'ghost tiny', text: 'Close', onclick: closeSharePanel }) + ]), + status + ); + await build(); + } else { + const input = el('textarea', { placeholder: 'Paste the shared text here (from -----BEGIN MARGINALIA----- to -----END MARGINALIA-----)', 'aria-label': 'Shared stickies text', spellcheck: 'false' }); + const file = el('input', { type: 'file', accept: '.txt,text/plain', hidden: true }); + file.addEventListener('change', async () => { if (file.files[0]) { input.value = await file.files[0].text(); doImport(); } }); + const doImport = async () => { + try { + if (!input.value.trim()) { status.className = 'share-status error'; status.textContent = 'Paste or choose the shared file first.'; return; } + const r = await send('shareImport', { text: input.value, passphrase: pass.value, url: current.tab?.url }); + status.className = 'share-status ok'; + status.textContent = `Added ${r.addedStickies} stick${r.addedStickies === 1 ? 'y' : 'ies'}${r.addedEntries ? ` and ${r.addedEntries} ${r.addedEntries === 1 ? 'entry' : 'entries'}` : ''} to “${cleanTitle(r.title)}”${r.samePage === false ? ' (a different page than this one)' : ''}.`; + if (r.samePage !== false) { await refresh({ keepEditor: true, capture: false }); if (!$('#share-panel').hidden) {/* keep the status visible */} } + if (r.samePage === false) toast(`Imported to ${cleanTitle(r.title)}`); + } catch (e) { status.className = 'share-status error'; status.textContent = e.message; } + }; + box.append( + el('div', { class: 'share-row' }, [el('b', { text: 'Import shared stickies' }), el('span', { class: 'spacer' }), el('button', { class: 'ghost tiny', text: 'Choose file…', onclick: () => file.click() }), file]), + input, + el('div', { class: 'share-row' }, [pass, el('button', { class: 'tiny primary', text: 'Import', onclick: doImport }), el('button', { class: 'ghost tiny', text: 'Close', onclick: closeSharePanel })]), + status + ); + input.focus(); + } +} + +// While the side panel or widget is open it is a long-lived page: watch the +// vault folder from here and ask the worker for a scoped sync as files land. +let pageObserver = null; +async function watchVaultFromPage() { + if (!PANEL || pageObserver || !canObserve()) return; + try { + const handle = await loadVaultHandle(); + if (!handle || (await handle.queryPermission({ mode: 'readwrite' })) !== 'granted') return; + const changes = changeCollector((names) => send('syncVault', { only: names ? [...names] : undefined }).catch(() => {})); + pageObserver = await observeHandle(handle, (change) => changes.add(change)); + } catch (e) { /* the alarm still runs */ } +} + +function renderVaultHint() { + const hint = $('#vault-hint'); + hint.hidden = current?.vault?.result !== 'no-permission'; +} + +function openDetail() { + if (current?.note) chrome.tabs.create({ url: chrome.runtime.getURL(`src/note.html?id=${current.note.id}`) }); +} + +function render({ keepEditor = false } = {}) { + const { ident, note, today } = current || {}; + // browser and extension pages hold no notes + const tab = current?.tab && /^(https?|file):/.test(current.tab.url || '') ? current.tab : null; + $('#empty').hidden = !!tab; + $('#empty').textContent = current?.tab ? 'Nothing to note on this page.' : 'No page here.'; + $('#compose').hidden = !tab; + $('#insights').hidden = !tab; + $('#entries').hidden = !tab; + $('#page-title').textContent = tab ? cleanTitle(note?.title || tab.title || tab.url) : SHORT_NAME; + $('#page-url').textContent = tab ? tab.url.replace(/^https?:\/\//, '') : ''; + const fav = $('#fav'); + fav.src = tab?.favIconUrl && !tab.favIconUrl.startsWith('chrome://') ? tab.favIconUrl : '../img/icon32.png'; + const rule = $('#rule'); + rule.hidden = !ident?.rule; + rule.textContent = ident?.rule || ''; + rule.title = ident ? `This note follows the page: ${ident.key}` : ''; + $('#open-detail').hidden = !note; + if (PANEL) $('#capture-hint').hidden = !tab || !!(note && note.snapshots && note.snapshots.length) || !/^https?:/.test(tab.url || ''); + + const savedCount = (note?.entries.length || 0) + (note?.stickies?.length || 0); + const savedHere = $('#saved-here'); + savedHere.hidden = !tab || !savedCount; + savedHere.textContent = `${savedCount} saved note${savedCount === 1 ? '' : 's'} on this page`; + if (noticedPage !== ident?.id) { + savedHere.classList.remove('arrived'); + if (savedCount) { void savedHere.offsetWidth; savedHere.classList.add('arrived'); } + noticedPage = ident?.id; + } + + // the editor holds today's entry + const editor = $('#entry'); + const todayEntry = note && today >= 0 ? note.entries[today] : null; + if (!keepEditor) { + editor.value = todayEntry ? todayEntry.text : ''; + lastSentText = editor.value; + } + $('#today-label').textContent = todayEntry ? `Today, started ${fmt(todayEntry.at).slice(11)}` : 'Today'; + + const rem = $('#reminder'); + rem.hidden = !note?.remind; + rem.innerHTML = ''; + if (note?.remind) rem.append(`⏰ ${fmt(note.remind)}`, el('button', { class: 'ghost', text: 'clear', onclick: () => setReminder({ when: null }) })); + document.querySelectorAll('#chips .chip[data-preset]').forEach((c) => c.classList.toggle('on', false)); + + renderInsights(current?.insights); + renderStickies(); + renderLinked(); + renderVaultHint(); + + const list = $('#entries'); + list.innerHTML = ''; + const entries = (note?.entries || []).map((e, i) => ({ ...e, i })).filter((e) => e.i !== today).reverse(); + for (const e of entries) { + const li = el('li', { class: 'entry' }); + const text = renderText(e.text); + const when = el('div', { class: 'when' }, [fmt(e.at), e.edited ? el('span', { class: 'muted', text: '(edited)' }) : null]); + when.append(el('span', { class: 'tools' }, [el('button', { class: 'ghost', text: 'delete', onclick: () => removeEntry(e.i) })])); + text.addEventListener('click', () => editInPlace(li, text, e)); + li.append(when, text); + list.append(li); + } + if (entries.length > 0) list.append(el('li', { class: 'hint', text: 'Click an entry to edit it. Changes save as you type.' })); +} + +async function refresh({ keepEditor = false, capture = false } = {}) { + const version = ++refreshVersion; + if (!await flushDraft() || version !== refreshVersion) return; + try { + // the popup is bound to the tab it opened on; the panel follows the active tab + const args = EMBED ? { tabId: ownTabId } : PANEL ? { windowId: (await chrome.windows.getCurrent()).id } : { tabId: current?.tab?.id }; + const editorText = $('#entry').value; + const next = await send('current', { ...args, capture }); + if (version !== refreshVersion) return; + const samePage = current?.ident?.id === next.ident?.id; + // Typing can continue while the page lookup is in flight. Save it to + // the old page before replacing the editor on a navigation. + if (!samePage && !await flushDraft()) return; + if (version !== refreshVersion) return; + keepEditor = samePage && (keepEditor || !!draftPromise || $('#entry').value !== lastSentText || $('#entry').value !== editorText); + current = next; + if (!samePage) setStatus(''); + } catch (e) { + if (version !== refreshVersion) return; + toast(e.message, true); + return; + } + render({ keepEditor }); +} + +// ----- autosave: the editor is today's entry -------------------- + +function scheduleDraft() { + const text = $('#entry').value; + if (text === lastSentText) return; + setStatus('Saving…', 'saving'); + clearTimeout(draftTimer); + draftTimer = setTimeout(flushDraft, 400); +} + +async function flushDraft() { + clearTimeout(draftTimer); + // Blur, navigation and the debounce timer can all request the same save. + // Await it, then drain any text typed while it was in flight. + if (draftPromise) { + if (!await draftPromise) return false; + return flushDraft(); + } + const text = $('#entry').value; + if (text === lastSentText || !current?.tab) return true; + const { tab, ident } = current; + draftPromise = (async () => { + try { + // The tab may already have navigated or closed. The displayed + // URL still identifies the note this editor belongs to. + const r = await send('draft', { tabId: tab.id, url: tab.url, title: current.note?.title || tab.title, text }); + if (current?.ident?.id === ident?.id) { + lastSentText = text; + current = { ...current, note: r.note, today: r.today }; + if ($('#entry').value === text) setStatus(text.trim() ? `Saved ${fmt(new Date().toISOString()).slice(11)}` : '', 'saved'); + render({ keepEditor: true }); + } + return true; + } catch (e) { + setStatus(e.message, 'error'); + return false; + } + })(); + const saved = await draftPromise; + draftPromise = null; + return saved ? flushDraft() : false; +} + +// ----- older entries: edit in place -------------------- + +function editInPlace(li, textNode, e) { + if (textNode.getAttribute('contenteditable') === 'true') return; + li.classList.add('editing'); + textNode.textContent = e.text; + textNode.setAttribute('contenteditable', 'true'); + textNode.focus(); + let timer = null; + const save = async () => { + const value = textNode.innerText.trim(); + if (value === e.text) return; + try { + const note = await send('updateEntry', { id: current.note.id, index: e.i, text: value }); + current = { ...current, note }; + e.text = value; + setStatus(`Saved ${fmt(new Date().toISOString()).slice(11)}`, 'saved'); + } catch (err) { toast(err.message, true); } + }; + textNode.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(save, 500); }); + textNode.addEventListener('blur', async () => { clearTimeout(timer); await save(); await refresh({ keepEditor: true }); }); +} + +async function removeEntry(index) { + try { await send('removeEntry', { id: current.note.id, index }); await refresh({ keepEditor: true }); } catch (e) { toast(e.message, true); } +} + +// ----- reminders and snooze -------------------- + +async function setReminder({ preset, when }) { + try { + if (!await flushDraft()) return; + if (!current.note || current.note.entries.length === 0) { + // a reminder needs something to come back to + const r = await send('draft', { tabId: current.tab.id, text: $('#entry').value.trim() || 'Reminder set.' }); + current = { ...current, note: r.note, today: r.today }; + } + await send('setReminder', { id: current.note.id, preset, when }); + toast(preset || when ? 'Reminder set' : 'Reminder cleared'); + await refresh({ keepEditor: true }); + } catch (e) { toast(e.message, true); } +} + +async function snoozeTab() { + try { + if (!await flushDraft()) return; + await send('snoozeTab', { tabId: current.tab.id, preset: 'tomorrow' }); + if (!PANEL || EMBED) dismiss(); + } catch (e) { toast(e.message, true); } +} + +function init() { + const editor = $('#entry'); + editor.addEventListener('input', scheduleDraft); + editor.addEventListener('blur', flushDraft); + document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') flushDraft(); }); + window.addEventListener('pagehide', flushDraft); + document.querySelectorAll('#chips .chip[data-preset]').forEach((b) => b.addEventListener('click', () => setReminder({ preset: b.dataset.preset }))); + $('#remind-at').addEventListener('change', (e) => { if (e.target.value) setReminder({ when: new Date(e.target.value).toISOString() }); }); + $('#snooze').addEventListener('click', snoozeTab); + $('#toggle-stickies').addEventListener('click', async () => { + try { + const visible = current.stickiesVisible === false; + await send('stickyVisibility', { visible }); + current.stickiesVisible = visible; + renderStickies(); + if (visible) await send('annotate', { tabId: current.tab.id }); + } catch (e) { toast(e.message, true); } + }); + $('#add-link').addEventListener('click', () => { if (picker) closeLinkPicker(); else openLinkPicker(); }); + $('#share-stickies').addEventListener('click', () => openSharePanel('export')); + $('#import-stickies').addEventListener('click', () => openSharePanel('import')); + $('#vault-allow').addEventListener('click', async () => { + try { + const handle = await loadVaultHandle(); + if (!handle) throw new Error('No vault folder is connected. Connect one in Settings.'); + if ((await handle.requestPermission({ mode: 'readwrite' })) !== 'granted') throw new Error('Access was not granted.'); + $('#vault-hint').hidden = true; + watchVaultFromPage(); + const r = await send('syncVault'); + toast(r.status === 'ok' ? `Vault synced: ${r.report.imported + r.report.updated} read in, ${r.report.written} written` : `Vault: ${r.status}`, r.status !== 'ok'); + await refresh({ keepEditor: true, capture: false }); + } catch (e) { toast(e.message, true); } + }); + $('#add-sticky').addEventListener('click', async () => { + try { + if (!await flushDraft()) return; + await send('annotate', { tabId: current.tab.id, pick: true }); + if (!PANEL || EMBED) dismiss(); + } catch (e) { toast(e.message, true); } + }); + $('#copy-feedback').addEventListener('click', async () => { + try { + const md = await send('feedback', { id: current.note.id }); + await navigator.clipboard.writeText(md); + toast('Feedback copied'); + } catch (e) { toast(e.message, true); } + }); + $('#always-show-toggle').addEventListener('change', async (e) => { + try { + if (e.target.checked) { + const origin = new URL(current.tab.url).origin; + const ok = await chrome.permissions.request({ origins: [`${origin}/*`] }); + if (!ok) throw new Error('Permission was not granted.'); + } + const r = await send('alwaysShow', { url: current.tab.url, enable: e.target.checked }); + e.target.checked = !!r.enabled; + toast(r.enabled ? `Stickies will show on ${r.origin.replace(/^https?:\/\//, '')}` : 'Turned off for this site'); + } catch (err) { e.target.checked = !e.target.checked; toast(err.message, true); } + }); + // "All notes" is the root of the hierarchy; the header arrow is one level up + $('#open-all').addEventListener('click', (e) => { e.preventDefault(); if (PANEL) showLibrary(true, { root: true }); else chrome.tabs.create({ url: chrome.runtime.getURL('src/options.html#notes') }); }); + $('#open-options').addEventListener('click', (e) => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); + $('#open-detail').addEventListener('click', (e) => { e.preventDefault(); openDetail(); }); + const openPanel = async (e) => { + e.preventDefault(); + if (!await flushDraft()) return; + try { + const w = await chrome.windows.getLastFocused(); + await chrome.sidePanel.open({ windowId: w.id }); + dismiss(); + } catch (err) { toast(err.message, true); } + }; + $('#open-panel')?.addEventListener('click', openPanel); + for (const id of ['#close-view', '#close-view-library']) $(id)?.addEventListener('click', async () => { if (await flushDraft()) dismiss(); }); + if (!PANEL || EMBED) { + // Esc closes the popup and the widget. The side panel stays: a stray + // Esc while writing must not take the whole panel away. + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { flushDraft().then((saved) => { if (saved) dismiss(); }); } }); + } + if (PANEL) { + $('#browse-notes').addEventListener('click', () => showLibrary()); + $('#library-up').addEventListener('click', () => { library?.up(); }); + $('#return-to-page').addEventListener('click', (e) => { e.preventDefault(); showLibrary(false); }); + const windowId = async () => (await chrome.windows.getCurrent()).id; + let switchTimer = null; + const follow = () => { clearTimeout(switchTimer); switchTimer = setTimeout(() => refresh({ capture: false }), 150); }; + if (EMBED) { + // bound to the tab around us; follow in-page navigations (pushState) of that tab + chrome.tabs.onUpdated.addListener((tabId, info) => { if (tabId === ownTabId && (info.url || info.status === 'complete')) follow(); }); + window.addEventListener('message', (e) => { + if (e.source !== window.parent || e.data?.type !== 'pn:shown') return; + refresh({ capture: false }).then(() => { if (!libraryVisible) editor.focus(); }); + }); + } else { + // follow the active tab: save what is being typed, then show the new page + chrome.tabs.onActivated.addListener(async (info) => { if (info.windowId === await windowId()) follow(); }); + chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => { if (tab.active && (info.url || info.status === 'complete') && tab.windowId === await windowId()) follow(); }); + } + // a note saved from the popup, the page or the context menu shows up here too + let libraryTimer; + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'local') return; + if (current?.ident && changes[`note:${current.ident.id}`]) refresh({ keepEditor: document.activeElement === editor, capture: false }); + if (changes['stickies:visible'] && current) { current.stickiesVisible = changes['stickies:visible'].newValue !== false; renderStickies(); } + if (libraryVisible && changes['notes:index']) { clearTimeout(libraryTimer); libraryTimer = setTimeout(() => library.reload(), 150); } + }); + } + // The popup's open is the user's click on this tab: capture now. The panel + // and the widget were opened by the worker, which already captured. + const start = async () => { + if (EMBED) ownTabId = (await chrome.tabs.getCurrent())?.id || (await chrome.tabs.query({ active: true, lastFocusedWindow: true }))[0]?.id || null; + await refresh({ capture: !PANEL }); + watchVaultFromPage(); + if (PANEL && !/^(https?|file):/.test(current?.tab?.url || '')) showLibrary(); + else { editor.focus(); editor.setSelectionRange(editor.value.length, editor.value.length); } + }; + start(); +} +init(); diff --git a/src/ui.css b/src/ui.css new file mode 100644 index 0000000..bb94573 --- /dev/null +++ b/src/ui.css @@ -0,0 +1,156 @@ +/* Design tokens shared by the popup, note and options pages. + Neutrals carry a slight indigo bias so greys read as chosen, not default. + Light is the base; dark redefines tokens only. */ +:root { + color-scheme: light; + --bg: #f6f6fa; + --panel: #ffffff; + --panel-2: #eff0f6; + --panel-3: #e6e8f1; + --text: #17192a; + --text-2: #3b3f55; + --muted: #6c7189; + --line: #e3e5ee; + --line-2: #d3d6e3; + --accent: #4f5bd5; + --accent-2: #3f49bd; + --accent-soft: #eceeff; + --accent-text: #3a44b3; + --danger: #c73b3b; + --danger-soft: #fdeaea; + --danger-text: #a52f2f; + --ok: #1f8a4c; + --ok-soft: #e4f6ec; + --warn: #b7791f; + --warn-soft: #fdf2dc; + --sticky-soft: #fff9e8; + --sticky-edge: #e8cb7e; + --sticky-ink: #795919; + --radius-s: 8px; + --radius: 12px; + --radius-l: 16px; + --shadow-s: 0 1px 2px rgba(23, 25, 42, 0.05), 0 1px 1px rgba(23, 25, 42, 0.03); + --shadow: 0 1px 2px rgba(23, 25, 42, 0.05), 0 8px 24px rgba(23, 25, 42, 0.07); + --shadow-l: 0 2px 4px rgba(23, 25, 42, 0.06), 0 20px 48px rgba(23, 25, 42, 0.12); + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; + --fs-xs: 11px; + --fs-s: 12px; + --fs-m: 13px; + --fs-l: 15px; + --fs-xl: 20px; + --ease: cubic-bezier(0.2, 0.7, 0.2, 1); +} +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + color-scheme: dark; + --bg: #13141c; + --panel: #1b1d29; + --panel-2: #232637; + --panel-3: #2c3043; + --text: #eceef6; + --text-2: #c3c7d8; + --muted: #9397ae; + --line: #2a2d3e; + --line-2: #383c52; + --accent: #7f8bff; + --accent-2: #98a3ff; + --accent-soft: #272b48; + --accent-text: #a9b1ff; + --danger: #ff6b6b; + --danger-soft: #3a2426; + --danger-text: #ff9b9b; + --ok: #4cd08a; + --ok-soft: #1e3a2c; + --warn: #e0a84a; + --warn-soft: #3c3220; + --sticky-soft: #302a1e; + --sticky-edge: #6d5830; + --sticky-ink: #f1ce7d; + --shadow-s: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + --shadow-l: 0 2px 4px rgba(0, 0, 0, 0.5), 0 20px 48px rgba(0, 0, 0, 0.5); + } +} +* { box-sizing: border-box; } +[hidden] { display: none !important; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font); + font-size: var(--fs-m); + line-height: 1.45; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + font-variant-numeric: tabular-nums; +} +a { color: var(--accent-text); text-decoration: none; } +a:hover { text-decoration: underline; text-underline-offset: 2px; } +button { + font: inherit; + color: inherit; + cursor: pointer; + border: 1px solid var(--line-2); + background: var(--panel); + border-radius: var(--radius-s); + padding: 6px 12px; + transition: background 0.15s var(--ease), border-color 0.15s var(--ease), color 0.15s var(--ease), box-shadow 0.15s var(--ease); +} +button:hover { background: var(--panel-2); } +button:active { transform: translateY(0.5px); } +button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [contenteditable]:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; box-shadow: var(--shadow-s); } +button.primary:hover { background: var(--accent-2); border-color: var(--accent-2); } +button.danger { background: var(--danger); border-color: var(--danger); color: #fff; font-weight: 600; } +button.ghost { background: transparent; border-color: transparent; color: var(--muted); } +button.ghost:hover { color: var(--text); background: var(--panel-2); } +button:disabled { opacity: 0.5; cursor: default; } +input[type="text"], input[type="number"], input[type="url"], input[type="search"], select, textarea { + font: inherit; + color: var(--text); + background: var(--panel); + border: 1px solid var(--line-2); + border-radius: var(--radius-s); + padding: 7px 10px; + transition: border-color 0.15s var(--ease), box-shadow 0.15s var(--ease); +} +input:hover, textarea:hover { border-color: var(--line-2); } +input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); outline: none; } +textarea { font-family: var(--font); line-height: 1.5; } +code, kbd { font-family: var(--mono); font-size: 11.5px; } +kbd { background: var(--panel-2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 1px 5px; color: var(--muted); } +.muted { color: var(--muted); } +.pill { + display: inline-flex; align-items: center; + padding: 2px 9px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent-text); + font-weight: 600; + font-size: var(--fs-xs); + letter-spacing: 0.01em; + white-space: nowrap; +} +.pill.warn { background: var(--danger-soft); color: var(--danger-text); } +.pill.ok { background: var(--ok-soft); color: var(--ok); } +.eyebrow { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); } +.list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); overflow: hidden; } +.list li { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-top: 1px solid var(--line); min-width: 0; } +.list li:first-child { border-top: 0; } +.list .t { flex: 1; min-width: 0; } +.list .t b { display: block; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.list .t span { display: block; color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +/* type-to-find picker (src/lib/picker.js) */ +.picker { position: relative; } +.picker input[type="search"] { width: 100%; } +.picker-list { list-style: none; margin: 4px 0 0; padding: 4px; border: 1px solid var(--line); border-radius: var(--radius-s); background: var(--panel); box-shadow: var(--shadow); max-height: 260px; overflow: auto; } +.picker-list:empty { display: none; } +.picker-list li { display: grid; gap: 1px; padding: 7px 9px; border-radius: 6px; cursor: pointer; } +.picker-list li.active, .picker-list li:hover { background: var(--accent-soft); } +.picker-title { font-size: var(--fs-s); font-weight: 550; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.picker-meta { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.picker-status { font-size: var(--fs-xs); padding: 4px 2px 0; min-height: 18px; } +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } } diff --git a/test/anchor.test.js b/test/anchor.test.js new file mode 100644 index 0000000..ddc35cc --- /dev/null +++ b/test/anchor.test.js @@ -0,0 +1,107 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { cssPathFor, describe, isStableId, resolve, similarity, structuralPathFor } from '../src/lib/anchor.js'; + +// A tiny DOM: enough of the surface anchor.js uses. +function node(tag, attrs = {}, children = []) { + const el = { tagName: tag.toUpperCase(), id: attrs.id || '', attrs, children: [], parentElement: null, text: attrs.text || '' }; + for (const c of children) { c.parentElement = el; el.children.push(c); } + Object.defineProperty(el, 'textContent', { get() { return el.text + el.children.map((c) => c.textContent).join(' '); } }); + el.getAttribute = (k) => attrs[k] || null; + return el; +} +function all(el, out = []) { out.push(el); el.children.forEach((c) => all(c, out)); return out; } +function matchesStep(el, step) { + if (step.startsWith('#')) return el.id === step.slice(1).replace(/\\(.)/g, '$1'); + if (step === 'body') return el.tagName === 'BODY'; + const m = /^(\w+):nth-of-type\((\d+)\)$/.exec(step); + if (!m) return false; + if (el.tagName.toLowerCase() !== m[1]) return false; + const sibs = el.parentElement ? el.parentElement.children.filter((s) => s.tagName === el.tagName) : [el]; + return sibs.indexOf(el) + 1 === Number(m[2]); +} +function root(html) { + const r = node('html', {}, [html]); + r.querySelectorAll = (sel) => sel === '*' ? all(r).slice(1) : all(r).filter((e) => e.tagName.toLowerCase() === sel.toLowerCase()); + r.querySelector = (sel) => { + const steps = sel.split(' > '); + const candidates = all(r).filter((e) => { + let n = e; + for (let i = steps.length - 1; i >= 0; i--) { + if (!n || !matchesStep(n, steps[i])) return false; + n = n.parentElement; + } + return true; + }); + return candidates[0] || null; + }; + return r; +} + +const page = () => { + const li2 = node('li', { text: 'Second point about pricing' }); + const ul = node('ul', {}, [node('li', { text: 'First point about speed' }), li2, node('li', { text: 'Third point about support' })]); + const h = node('h2', { text: 'Pricing' }); + const p = node('p', { text: 'Plans start at $9 per month, billed yearly.' }); + const section = node('section', { id: 'pricing' }, [h, p, ul]); + const intro = node('p', { text: 'Welcome to the product.' }); + const body = node('body', {}, [node('div', { id: '__next' }, [node('main', {}, [intro, section])])]); + return { root: root(body), li2, p, h, intro }; +}; + +test('isStableId rejects generated ids', () => { + assert.ok(isStableId('pricing')); + assert.ok(isStableId('main-nav')); + assert.ok(!isStableId('__next')); + assert.ok(!isStableId('react-select-2-input'.replace('react-select', 'react'))); + assert.ok(!isStableId('a1b2c3d4e5f6a7b8')); + assert.ok(!isStableId('item-1234567')); + assert.ok(!isStableId(':r1:')); +}); + +test('cssPathFor roots at the nearest stable id; structural path reaches body', () => { + const { li2, p } = page(); + assert.equal(cssPathFor(li2), '#pricing > ul:nth-of-type(1) > li:nth-of-type(2)'); + assert.equal(cssPathFor(p), '#pricing > p:nth-of-type(1)'); + assert.equal(structuralPathFor(p), 'body > div:nth-of-type(1) > main:nth-of-type(1) > section:nth-of-type(1) > p:nth-of-type(1)'); +}); + +test('describe captures a quote with context', () => { + const { root: r, li2 } = page(); + const a = describe(li2, r); + assert.equal(a.quote.exact, 'Second point about pricing'); + assert.match(a.quote.prefix, /First point about speed ?$/); + assert.match(a.quote.suffix, /^ Third point/); + assert.equal(a.tag, 'li'); +}); + +test('resolve: css first, then quote when the structure moved, orphan when the text is gone', () => { + const { root: r, li2 } = page(); + const a = describe(li2, r); + assert.equal(resolve(a, r).how, 'css'); + assert.equal(resolve(a, r).el, li2); + // the list gained an item at the front: css points at the wrong li, text search recovers it + const moved = page(); + moved.li2.parentElement.children.unshift(node('li', { text: 'A brand new first point' })); + moved.li2.parentElement.children[0].parentElement = moved.li2.parentElement; + const r2 = resolve(a, moved.root); + assert.equal(r2.how, 'quote'); + assert.equal(r2.el.textContent, 'Second point about pricing'); + // the text changed completely: orphan, not a guess + const gone = page(); + gone.li2.text = 'Something else entirely now'; + const r3 = resolve(a, gone.root); + assert.equal(r3.how, 'orphan'); + assert.equal(r3.el, null); +}); + +test('resolve verifies by similarity, so a slightly edited element still matches', () => { + const { root: r, p } = page(); + const a = describe(p, r); + p.text = 'Plans start at $12 per month, billed yearly.'; + const res = resolve(a, r); + assert.equal(res.how, 'css'); + assert.ok(res.confidence >= 0.6, String(res.confidence)); + assert.ok(similarity('a b c d', 'a b c d') === 1); + assert.ok(similarity('a b c d', 'x y z') === 0); +}); diff --git a/test/fixtures/linkedin-clean.txt b/test/fixtures/linkedin-clean.txt new file mode 100644 index 0000000..64cd58a --- /dev/null +++ b/test/fixtures/linkedin-clean.txt @@ -0,0 +1,18 @@ +Sam Example +Customer Success Engineer at Dataro +Sydney, New South Wales, Australia +Experience +Dataro +Full-time · 2 yrs 1 mo +Senior Customer Success Engineer +Jan 2025 - Present · 1 yr 10 mos +Customer Success Engineer +Oct 2024 - Dec 2024 · 3 mos +Support Engineer +Hooli · Full-time +Jan 2020 - Sep 2024 · 4 yrs 9 mos +Melbourne, Victoria, Australia +Education +University of Melbourne +Bachelor of Engineering, Software +2015 - 2019 diff --git a/test/fixtures/linkedin-overlap.txt b/test/fixtures/linkedin-overlap.txt new file mode 100644 index 0000000..f4fe072 --- /dev/null +++ b/test/fixtures/linkedin-overlap.txt @@ -0,0 +1,34 @@ +Jane Doe +Head of Things at Acme | ex-Globex +San Francisco, California, United States +500+ connections +About +I build things. +…see more +Experience +Head of Things +Acme Corp · Full-time +Mar 2023 - Present · 3 yrs 8 mos +San Francisco Bay Area · Hybrid +Globex +Full-time · 4 yrs 2 mos +Senior Engineer +Jan 2021 - Feb 2025 · 4 yrs 2 mos +Remote +Engineer +Nov 2020 - Dec 2020 · 2 mos +Austin, Texas, United States +Consultant +Initech · Contract +Jun 2024 - Present · 2 yrs 5 mos +Analyst +Umbrella Co · Full-time +Feb 2016 - Sep 2016 · 8 mos +Chicago, Illinois, United States +Show all 7 experiences +Education +Stanford University +Bachelor of Science - BS, Computer Science +2012 - 2016 +Skills +Python · SQL diff --git a/test/fixtures/linkedin-varied.txt b/test/fixtures/linkedin-varied.txt new file mode 100644 index 0000000..b67d92c --- /dev/null +++ b/test/fixtures/linkedin-varied.txt @@ -0,0 +1,54 @@ +Alex Example +Alex Example +Founder & CEO at Example Labs · Angel investor +Founder & CEO at Example Labs · Angel investor +Austin, Texas, United States +Austin, Texas, United States +2,314 followers +About +About +Over 12 years building data products. Open to board roles. +Experience +Experience +Founder & CEO +Founder & CEO +Example Labs · Self-employed +Example Labs · Self-employed +Jun 2022 – present · 4 yrs 5 mos +Jun 2022 – present · 4 yrs 5 mos +Austin, Texas, United States +Austin, Texas, United States +Advisor +Advisor +Startup Collective +Startup Collective +2021 – 2023 +2021 – 2023 +Globex +Globex +Full-time · 6 yrs 3 mos +Full-time · 6 yrs 3 mos +Director of Data +Director of Data +Jan 2019 – May 2022 · 3 yrs 5 mos +Jan 2019 – May 2022 · 3 yrs 5 mos +Remote +Remote +Data Engineer +Data Engineer +Mar 2016 – Dec 2018 · 2 yrs 10 mos +Mar 2016 – Dec 2018 · 2 yrs 10 mos +Data Analyst +Data Analyst +Initech · Internship +Initech · Internship +Jun 2015 – Aug 2015 · 3 mos +Jun 2015 – Aug 2015 · 3 mos +Education +Education +University of Texas at Austin +University of Texas at Austin +2011 – 2015 +2011 – 2015 +Licenses & certifications +AWS Certified Solutions Architect diff --git a/test/notes.test.js b/test/notes.test.js new file mode 100644 index 0000000..a3d7b26 --- /dev/null +++ b/test/notes.test.js @@ -0,0 +1,256 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { addEntry, fileNameFor, fromMarkdown, identify, indexEntry, linkNotes, matchesQuery, mergeNotes, newNote, noteId, presetTime, removeEntry, sanitizeSavedTitle, setReminder, toMarkdown, unlinkNotes, updateEntry } from '../src/lib/notes.js'; +import { DEFAULT_RULES, compileRules } from '../src/lib/url.js'; + +const { compiled } = compileRules(DEFAULT_RULES); + +test('saved LinkedIn titles lose unread counts without changing note history or identity', () => { + const note = { ...newNote({ url: 'https://www.linkedin.com/in/jane/', title: 'Jane | LinkedIn', key: 'linkedin:jane' }), title: '(25) Jane | LinkedIn', snapshots: [{ title: '(25) Jane | LinkedIn', text: 'Historical capture' }] }; + const cleaned = sanitizeSavedTitle(note); + assert.equal(cleaned.title, 'Jane | LinkedIn'); + assert.deepEqual({ ...cleaned, title: note.title }, note); + assert.equal(sanitizeSavedTitle(cleaned), cleaned); + const other = { ...note, url: 'https://example.com/article', title: '(2026) Annual report' }; + assert.equal(sanitizeSavedTitle(other), other); + assert.equal(sanitizeSavedTitle({ ...note, url: 'https://linkedin.com.example.com/' }).title, note.title); +}); + +test('search finds sticky comments and anchored quotes', () => { + const note = { ...newNote({ url: 'https://example.com/', key: 'example', title: 'Example' }), stickies: [{ text: 'Clarify annual billing', anchor: { quote: { exact: 'Plans start at $9' } } }] }; + assert.equal(matchesQuery(note, 'annual billing'), true); + assert.equal(matchesQuery(note, 'plans $9'), true); + assert.equal(matchesQuery(note, 'monthly discount'), false); +}); + +test('identify collapses URL variants to one note id', () => { + const a = identify('https://github.com/dataro/app/pull/482', compiled, {}); + const b = identify('https://github.com/dataro/app/pull/482/files#diff', compiled, {}); + assert.equal(a.id, b.id); + assert.equal(a.rule, 'GitHub pull request'); + assert.notEqual(a.id, identify('https://github.com/dataro/app/pull/483', compiled, {}).id); + assert.match(noteId('x'), /^[0-9a-f]{12}$/); +}); + +test('entries are dated and ordered; empty text is ignored', () => { + let n = newNote({ url: 'https://example.com/a', title: 'A', key: 'https://example.com/a', now: new Date('2026-10-11T01:00:00Z') }); + n = addEntry(n, ' ', new Date('2026-10-11T02:00:00Z')); + assert.equal(n.entries.length, 0); + n = addEntry(n, 'first', new Date('2026-10-11T02:00:00Z')); + n = addEntry(n, 'second', new Date('2026-10-12T02:00:00Z')); + assert.deepEqual(n.entries.map((e) => e.text), ['first', 'second']); + assert.equal(n.updated, '2026-10-12T02:00:00.000Z'); + n = updateEntry(n, 0, 'first, edited'); + assert.equal(n.entries[0].text, 'first, edited'); + assert.ok(n.entries[0].edited); + n = removeEntry(n, 1); + assert.equal(n.entries.length, 1); + n = setReminder(n, '2026-11-01T09:00:00Z'); + assert.equal(n.remind, '2026-11-01T09:00:00.000Z'); +}); + +test('markdown round-trips with frontmatter and dated sections', () => { + let n = newNote({ url: 'https://www.linkedin.com/in/jane/', title: 'Jane Doe | LinkedIn', key: 'linkedin:in/jane', rule: 'LinkedIn profile', now: new Date('2026-10-11T01:00:00Z') }); + n = addEntry(n, 'Likes cheese.\nTwo kids, dog named Fluffy.', new Date('2026-10-11T03:02:00Z')); + n = addEntry(n, '> quoted line\nmet at the meetup', new Date('2026-10-18T22:30:00Z')); + n = { ...n, tags: ['people', 'sales'], remind: '2026-11-01T09:00:00.000Z', snapshot: { at: n.updated, text: 'Jane Doe - Head of Things at Acme "quoted"' } }; + const md = toMarkdown(n); + assert.match(md, /^---\ntitle: "Jane Doe \| LinkedIn"\nurl: https:\/\/www\.linkedin\.com\/in\/jane\/\nkey: linkedin:in\/jane\nrule: "LinkedIn profile"\n/); + assert.match(md, /tags: \[people, sales\]/); + assert.match(md, /excerpt: "Jane Doe - Head of Things at Acme \\"quoted\\""/); + // newest entry first in the file + assert.ok(md.indexOf('met at the meetup') < md.indexOf('Likes cheese')); + const back = fromMarkdown(md); + assert.equal(back.id, n.id); + assert.equal(back.key, n.key); + assert.equal(back.title, n.title); + assert.deepEqual(back.tags, ['people', 'sales']); + assert.equal(back.remind, n.remind); + assert.deepEqual(back.entries.map((e) => e.text), n.entries.map((e) => e.text)); + // headings are local wall-clock; the parsed timestamp must match to the minute + assert.equal(Math.abs(new Date(back.entries[0].at) - new Date(n.entries[0].at)) < 60000, true); + assert.equal(fileNameFor(n), `jane-doe-linkedin-${n.id}.md`); +}); + +test('fromMarkdown accepts hand-written notes and rejects junk', () => { + const hand = `---\ntitle: Plain\nurl: https://example.com/x\nkey: https://example.com/x\n---\nJust a paragraph with no heading.\n\n## 2026-10-12\nA dated one.\n`; + const n = fromMarkdown(hand); + assert.equal(n.entries.length, 2); + assert.equal(n.entries[1].text, 'A dated one.'); + assert.throws(() => fromMarkdown('no frontmatter'), /frontmatter/); + assert.throws(() => fromMarkdown('---\ntitle: x\n---\n'), /url and key/); +}); + +test('mergeNotes unions entries without duplicates', () => { + let a = newNote({ url: 'https://e.com/', title: 'E', key: 'https://e.com/', now: new Date('2026-01-01T00:00:00Z') }); + a = addEntry(a, 'one', new Date('2026-01-02T00:00:00Z')); + let b = addEntry(a, 'two', new Date('2026-01-03T00:00:00Z')); + b = { ...b, tags: ['t'] }; + const m = mergeNotes(a, b); + assert.deepEqual(m.entries.map((e) => e.text), ['one', 'two']); + assert.deepEqual(m.tags, ['t']); + assert.deepEqual(mergeNotes(null, b), b); +}); + +test('matchesQuery and presets', () => { + const n = addEntry(newNote({ url: 'https://e.com/', title: 'Acme plan', key: 'k' }), 'budget for Q4'); + assert.ok(matchesQuery(n, 'acme q4')); + assert.ok(!matchesQuery(n, 'zebra')); + const now = new Date('2026-10-11T10:00:00'); + assert.equal(presetTime('1h', now), new Date('2026-10-11T11:00:00').toISOString()); + assert.equal(presetTime('tomorrow', now), new Date('2026-10-12T09:00:00').toISOString()); + assert.equal(new Date(presetTime('nextweek', now)).getDay(), 1); + assert.equal(presetTime('nope', now), null); +}); + +test('addSnapshot keeps history, dedupes identical captures, caps length and keeps the first', async () => { + const { addSnapshot, SNAPSHOT_LIMIT } = await import('../src/lib/notes.js'); + let n = newNote({ url: 'https://e.com/', title: 'E', key: 'k' }); + n = addSnapshot(n, { title: 'E', text: 'v1', meta: { siteName: 'E' } }, new Date('2026-01-01T00:00:00Z')); + n = addSnapshot(n, { title: 'E', text: 'v1' }, new Date('2026-01-02T00:00:00Z')); + assert.equal(n.snapshots.length, 1); + assert.equal(n.snapshots[0].seen, '2026-01-02T00:00:00.000Z'); + assert.equal(n.snapshots[0].at, '2026-01-01T00:00:00.000Z'); + n = addSnapshot(n, { title: 'E', text: 'v2', structured: { kind: 'x' } }, new Date('2026-01-03T00:00:00Z')); + assert.equal(n.snapshots.length, 2); + assert.equal(n.snapshot.text, 'v2'); + assert.equal(n.snapshots[1].structured.kind, 'x'); + for (let i = 0; i < SNAPSHOT_LIMIT + 5; i++) n = addSnapshot(n, { title: 'E', text: `v${i + 10}` }, new Date(2026, 1, i + 1)); + assert.equal(n.snapshots.length, SNAPSHOT_LIMIT); + assert.equal(n.snapshots[0].text, 'v1'); + assert.equal(addSnapshot(n, null), n); + assert.equal(addSnapshot(n, { text: '' }), n); +}); + +test('mergeNotes matches entries to the minute and keeps the existing reminder', () => { + let a = newNote({ url: 'https://e.com/', title: 'E', key: 'https://e.com/', now: new Date('2026-01-01T00:00:00Z') }); + a = addEntry(a, 'one', new Date('2026-01-02T03:04:17.123Z')); + // the same note after a Markdown round-trip: seconds are gone + const back = fromMarkdown(toMarkdown(a)); + assert.equal(back.entries[0].at.slice(17), '00.000Z'); + const m = mergeNotes(a, back, new Date('2026-06-01T00:00:00Z')); + assert.equal(m.entries.length, 1, 'round-tripped entry must not duplicate'); + // reminders: existing wins; a new note only takes a future reminder + const withPast = { ...back, remind: '2026-01-05T09:00:00.000Z' }; + assert.equal(mergeNotes({ ...a, remind: null }, withPast, new Date('2026-06-01T00:00:00Z')).remind, null); + assert.equal(mergeNotes(null, withPast, new Date('2026-06-01T00:00:00Z')).remind, null); + const withFuture = { ...back, remind: '2027-01-05T09:00:00.000Z' }; + assert.equal(mergeNotes(null, withFuture, new Date('2026-06-01T00:00:00Z')).remind, '2027-01-05T09:00:00.000Z'); + assert.equal(mergeNotes({ ...a, remind: '2026-12-25T00:00:00.000Z' }, withFuture).remind, '2026-12-25T00:00:00.000Z'); +}); + +test('addSnapshot ignores an identical capture within the hour', async () => { + const { addSnapshot } = await import('../src/lib/notes.js'); + let n = newNote({ url: 'https://e.com/', title: 'E', key: 'k' }); + n = addSnapshot(n, { title: 'E', text: 'v1' }, new Date('2026-01-01T00:00:00Z')); + const same = addSnapshot(n, { title: 'E', text: 'v1' }, new Date('2026-01-01T00:30:00Z')); + assert.equal(same, n); + const later = addSnapshot(n, { title: 'E', text: 'v1' }, new Date('2026-01-01T02:00:00Z')); + assert.notEqual(later, n); + assert.equal(later.snapshots[0].seen, '2026-01-01T02:00:00.000Z'); +}); + +test('setToday creates, updates and removes the day\'s entry without duplicating it', async () => { + const { setToday, todayIndex, localDay } = await import('../src/lib/notes.js'); + const day = new Date('2026-10-11T09:00:00'); + let n = newNote({ url: 'https://e.com/', title: 'E', key: 'k', now: new Date('2026-10-10T09:00:00') }); + n = addEntry(n, 'yesterday', new Date('2026-10-10T09:00:00')); + assert.equal(todayIndex(n, day), -1); + n = setToday(n, 'first words', day); + assert.equal(n.entries.length, 2); + assert.equal(todayIndex(n, day), 1); + const later = new Date('2026-10-11T17:30:00'); + n = setToday(n, 'first words, then more', later); + assert.equal(n.entries.length, 2, 'same day edits the entry'); + assert.equal(n.entries[1].text, 'first words, then more'); + assert.equal(localDay(n.entries[1].at), '2026-10-11', 'keeps the first timestamp of the day'); + assert.ok(n.entries[1].edited); + const unchanged = setToday(n, 'first words, then more', later); + assert.equal(unchanged, n, 'identical text is a no-op'); + n = setToday(n, ' ', later); + assert.equal(n.entries.length, 1, 'emptying removes the day'); + assert.equal(setToday(n, '', later), n); +}); + +test('cleanTitle strips unread counters and keeps the site name', async () => { + const { cleanTitle } = await import('../src/lib/notes.js'); + assert.equal(cleanTitle('(3) Jane Doe | LinkedIn'), 'Jane Doe | LinkedIn'); + assert.equal(cleanTitle('(12+) Jane Doe | LinkedIn'), 'Jane Doe | LinkedIn'); + assert.equal(cleanTitle('[2] Inbox - Gmail'), 'Inbox - Gmail'); + assert.equal(cleanTitle('• (1) Slack | general'), 'Slack | general'); + assert.equal(cleanTitle('Fix consolidate target · Pull Request #482 · dataro/app'), 'Fix consolidate target · Pull Request #482 · dataro/app'); + assert.equal(cleanTitle(' Spaced out '), 'Spaced out'); + assert.equal(cleanTitle('(1) '), '(1)'); + assert.equal(cleanTitle(''), ''); +}); + +test('a capture with a new title renames the note; newNote cleans its title', async () => { + const { addSnapshot } = await import('../src/lib/notes.js'); + let n = newNote({ url: 'https://www.linkedin.com/in/jane/', title: '(4) Jane Doe | LinkedIn', key: 'linkedin:in/jane' }); + assert.equal(n.title, 'Jane Doe | LinkedIn'); + n = addSnapshot(n, { title: '(9) Jane D. | LinkedIn', text: 'v1', meta: { siteName: 'LinkedIn' } }, new Date('2026-01-01T00:00:00Z')); + assert.equal(n.title, 'Jane D. | LinkedIn'); + assert.equal(n.snapshots[0].title, '(9) Jane D. | LinkedIn', 'the capture keeps the raw title'); +}); + +test('stickies: add, update, remove, feedback, and Markdown round-trip', async () => { + const { addSticky, updateSticky, removeSticky, feedbackMarkdown } = await import('../src/lib/notes.js'); + let n = newNote({ url: 'https://site.test/pricing', title: 'Pricing – Site', key: 'https://site.test/pricing', now: new Date('2026-10-15T01:00:00Z') }); + const anchor = { css: '#pricing > p:nth-of-type(1)', path: 'body > main:nth-of-type(1) > p:nth-of-type(1)', quote: { exact: 'Plans start at $9 per month', prefix: 'Pricing ', suffix: ', billed' }, tag: 'p', label: '' }; + n = addSticky(n, { anchor, text: 'Say "from $9/mo" and lead with annual savings.' }, new Date('2026-10-15T01:00:00Z')); + n = addSticky(n, { anchor: { ...anchor, css: '#pricing > h2:nth-of-type(1)', quote: { exact: 'Pricing', prefix: '', suffix: '' }, tag: 'h2' }, text: 'Too small on mobile.' }); + assert.equal(n.stickies.length, 2); + const id = n.stickies[0].id; + n = updateSticky(n, id, { text: 'Say "from $9/mo".' }); + assert.equal(n.stickies[0].text, 'Say "from $9/mo".'); + assert.equal(updateSticky(n, id, { text: 'Say "from $9/mo".' }), n, 'no change is a no-op'); + const fb = feedbackMarkdown(n); + assert.match(fb, /^Feedback on Pricing – Site\nhttps:\/\/site\.test\/pricing\n\n1\. "Plans start at \$9 per month"\n Say "from \$9\/mo"\.\n2\. "Pricing"\n Too small on mobile\.$/); + const md = toMarkdown(n); + assert.match(md, /## Stickies\n\n1\. \*\*"Plans start at \$9 per month"\*\* — Say "from \$9\/mo"\.\n