diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2b494f00aa6..000a7329776 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,7 @@ - - - - - +## Description -## Description of what has changed - - - +## Problem Solved -## Issues addressed by pull request - - - - - - - +## Alternatives Considered + +## Related issue(s) \ No newline at end of file diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 05b7d9b98a1..396a96412a0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -20,10 +20,10 @@ jobs: python-version: '3.13' cache: 'pip' - - name: Setup Node.js environment - uses: actions/setup-node@v4 + - name: Install Just + uses: extractions/setup-just@v3 with: - node-version: '26.x' + just-version: '1.58.0' - name: Upgrade pip run: python3 -m pip install --upgrade pip @@ -44,7 +44,7 @@ jobs: - name: Build website run: >- - python3 update-attack.py --attack-brand + just build-website --attack-brand --all-extras --no-test-exitstatus env: @@ -59,14 +59,6 @@ jobs: - name: Remove STIX directory run: rm -rf output/stix/ - - name: Build ATT&CK Search module - run: | - cd attack-search - npm ci - npm run build - cp dist/search_bundle.js ../output/theme/scripts/ - cd .. - - name: Add BlueSky Identification if: ${{ vars.BLUESKY_ID != '' }} run: | diff --git a/.gitignore b/.gitignore index 490a4594a60..6a0d5eec6e0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,10 +38,12 @@ attack-theme/templates/general/sidebar-resources.html content/ data/pelican_settings.json -# this file is generated by the search module and should not be committed -search_bundle.js node_modules/ -attack-style/dist/ +# Intermediate attack-search and attack-style outputs +/attack-search/dist/ +/attack-search/compilation-stats.json +/attack-style/dist/ + attack-version-archives/ tmp/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..6f4247a6255 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +26 diff --git a/AGENTS.md b/AGENTS.md index ee0934a70cb..8f433d781cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ This file is guidance for coding agents working in `attack-website`. - `attack-search/` is a separate Node/CommonJS project for the search bundle. - Search source and tests live in `attack-search/src/` and `attack-search/__tests__/`. - `attack-style/` is a separate Node/Sass project for CSS output. -- SCSS entrypoints are `attack-style/style-attack.scss` and `attack-style/style-user.scss`. +- SCSS entrypoints are `attack-style/style-attack.scss`, `attack-style/style-user.scss`, and `attack-style/style-archive.scss`. - `attack-theme/` contains Jinja templates, static assets, and legacy browser JS. - Theme templates and static assets live in `attack-theme/templates/` and `attack-theme/static/`. - `modules/` contains Python modules that generate ATT&CK site content. @@ -28,7 +28,8 @@ This file is guidance for coding agents working in `attack-website`. - When managing a local Python environment, prefer `uv` with a virtual environment at `.venv` in the git repository root. - Node.js and npm are required for `attack-search/` and `attack-style/`. - Docker is the preferred way to validate the final static output in an Nginx-like environment. -- CI currently uses Python `3.13` and Node `18.x` in `.github/workflows/gh-pages.yml`. +- Just `1.58.0` or newer is required for shared build commands. CI and Docker pin Just `1.58.0`. +- CI uses Python `3.13`; Docker uses Python `3.13` and Node `26`. CI consumes committed assets without npm. - Prefer CI versions when reproducing CI behavior; Docker and development docs may reference older base images. - Production-like builds may depend on environment variables from `.github/workflows/gh-pages.yml`, including `ATTACK_WEBSITE_GOOGLE_ANALYTICS`, `ATTACK_WEBSITE_GOOGLE_SITE_VERIFICATION`, `ATTACK_WEBSITE_INCLUDE_OSANO`, and `PELICAN_SITEURL`. @@ -38,7 +39,8 @@ Run commands from the repo root unless a subdirectory is called out. ### Install -- Preferred Python env: `uv venv .venv` +- All local dependencies: `just install-deps` (creates/reuses root `.venv` and runs `npm ci` in both packages) +- Preferred Python env for manual setup: `uv venv --python 3.13 .venv` - Python deps: `uv pip install -r requirements.txt` - Search deps: `cd attack-search && npm ci` - Style deps: `cd attack-style && npm ci` @@ -46,10 +48,16 @@ Run commands from the repo root unless a subdirectory is called out. ### Build -- Main website build: `uv run python update-attack.py --attack-brand --all-extras --no-test-exitstatus` +- Complete website build: `just build-full-website --attack-brand --all-extras` +- Website using staged assets: `just build-website --attack-brand --all-extras` +- Compile and stage assets: `just build-search`, `just build-style`, or `just build-assets` +- Website targets retain Python CLI defaults unless flags are explicitly supplied; builds do not install dependencies. +- Python generator and targeted checks: `uv run python update-attack.py ...` +- Shared build ordering, prerequisites, and Docker integration: `docs/DEVELOPMENT.md` - Search bundle: `cd attack-search && npm run build` - Search dev bundle: `cd attack-search && npm run build:dev` -- Copy built search bundle into site output: `cd attack-search && npm run copy` +- Copy the compiled search bundle into theme static assets: `cd attack-search && npm run copy` +- Search build + copy into theme static assets: `cd attack-search && npm run build-copy` - Style build: `cd attack-style && npm run build` - Style build + copy into theme static assets: `cd attack-style && npm run build-copy` @@ -81,8 +89,8 @@ Run commands from the repo root unless a subdirectory is called out. ### Important Command Notes -- There is no root `package.json`, `Makefile`, or single universal test runner. -- CI clearly builds the site and search bundle, but does not currently enforce Jest, ESLint, Stylelint, Ruff, or type checks. +- `justfile` defines the shared asset/site build commands. There is no root `package.json`, `Makefile`, or single universal test runner. +- CI builds the site using committed assets without rebuilding or verifying them against source. Docker rebuilds assets. CI does not currently enforce Jest, ESLint, Stylelint, Ruff, or type checks. - For Python-side testing, the narrowest supported scope is a named category (`size`, `links`, `external_links`, `citations`), not an individual test file. - Preferred production-like validation is Nginx via Docker, not Pelican's built-in dev server. - Pelican's built-in development server does not match production Nginx routing behavior. @@ -99,8 +107,10 @@ Run commands from the repo root unless a subdirectory is called out. - Do not edit `output/` as source; regenerate it through the build pipeline. - Avoid direct edits to `attack-search/dist/` and `attack-style/dist/` unless the task explicitly targets generated artifacts. -- Avoid direct edits to copied assets in `attack-theme/static/` when a source file in `attack-style/` or another generator owns the output. +- Avoid direct edits to copied assets in `attack-theme/static/` when a source file in `attack-style/`, `attack-search/` or another generator owns the output. - Preserve generated-file comments and edit the named source template or source asset instead. +- Ignore intermediate outputs in `attack-search/dist/` and `attack-style/dist/`, but commit the compiled theme CSS and `attack-theme/static/scripts/search_bundle.js` together with frontend source changes. After changing anything in either package, regenerate its assets with `just build-search`, `just build-style`, or `just build-assets`. Keeping them current is the developer's responsibility. +- Keep the build-specific `attack-theme/static/scripts/settings.js` ignored; see `docs/DEVELOPMENT.md` for its runtime configuration role. ## Python Style diff --git a/CHANGELOG.md b/CHANGELOG.md index 2039c128b43..3e6771534b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Website Changelog +## v5.1.0 (2026-09-21) + +### Features + +* Add dark mode! +* Move Docker's Node and Python build stages to Debian Trixie. +* Add Just build commands for Search, Style, assets, and website generation. + +### Bug Fixes + +* Improve search results for exact ATT&CK ID and numeric ID queries by prioritizing matching object pages and relevant references. +* Fix sidebar loading for HTTPS redirects. +* Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up. +* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. + ## v5.0.0 (2026-08-06) * Release ATT&CK content version 19.2. diff --git a/Dockerfile b/Dockerfile index 599f1957381..237b67b1bc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,37 @@ # syntax=docker/dockerfile:1.7 -FROM node:26-bookworm-slim AS search-build +FROM node:26-trixie-slim AS assets-build -WORKDIR /src/attack-search +ARG ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND=":" -COPY attack-search/package*.json ./ -RUN npm ci +RUN apt update \ + && apt install -y --no-install-recommends ca-certificates curl \ + && sh -ec "${ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND}" \ + && rm -rf /var/lib/apt/lists/* -COPY attack-search/webpack.config.cjs ./ -COPY attack-search/src ./src -RUN npm run build +# Install pre-built Just (official installer) +RUN curl --proto '=https' --tlsv1.2 -fsSL https://just.systems/install.sh -o /tmp/install-just.sh \ + && bash /tmp/install-just.sh --tag 1.58.0 --to /usr/local/bin \ + && rm /tmp/install-just.sh \ + && test "$(just --version)" = "just 1.58.0" + +WORKDIR /src/attack-website +COPY attack-search/package*.json ./attack-search/ +COPY attack-style/package*.json ./attack-style/ +RUN npm --prefix attack-search ci && npm --prefix attack-style ci -FROM python:3.13-slim-bookworm AS site-base +COPY justfile ./ +COPY attack-search/webpack.config.cjs ./attack-search/ +COPY attack-search/.babelrc ./attack-search/ +COPY attack-search/src ./attack-search/src +COPY attack-style/ ./attack-style/ + +# Regenerate the committed theme assets using the same copy commands as local builds. +RUN just build-assets + + +FROM python:3.13-slim-trixie AS site-base ARG PELICAN_SITEURL="" ARG ATTACK_WEBSITE_BANNER_ENABLED="" @@ -55,11 +74,16 @@ ENV PELICAN_SITEURL=${PELICAN_SITEURL} \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl git \ +RUN apt update \ + && apt install -y --no-install-recommends ca-certificates curl git \ && sh -ec "${ATTACK_WEBSITE_OS_CA_TRUST_SETUP_COMMAND}" \ && rm -rf /var/lib/apt/lists/* +RUN curl --proto '=https' --tlsv1.2 -fsSL https://just.systems/install.sh -o /tmp/install-just.sh \ + && bash /tmp/install-just.sh --tag 1.58.0 --to /usr/local/bin \ + && rm /tmp/install-just.sh \ + && test "$(just --version)" = "just 1.58.0" + WORKDIR /src/attack-website COPY requirements.txt ./ @@ -69,9 +93,8 @@ RUN python3 -m pip install --no-cache-dir wheel \ COPY . ./ -# The generator copies theme assets into output/, so place the generated search bundle -# in the theme before running it. -COPY --from=search-build /src/attack-search/dist/search_bundle.js attack-theme/static/scripts/search_bundle.js +# Copy the complete staged asset set before Pelican renders and preserves the site. +COPY --from=assets-build /src/attack-website/attack-theme/static/ attack-theme/static/ FROM site-base AS website-build @@ -94,7 +117,7 @@ RUN --mount=type=secret,id=workbench_api_key,required=false \ set -- "$@" --extras "$extra"; \ done; \ fi; \ - python3 update-attack.py "$@" \ + just build-website "$@" \ --version-archive-dir "${ATTACK_WEBSITE_VERSION_ARCHIVE_DIR}" diff --git a/README.md b/README.md index 041dc4cd4df..5d0da86f055 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ If you find errors or typos in the site content, let us know by sending an email Check out our [developer guide](docs/DEVELOPMENT.md) if you are interested in extending the style, content, or functionality of this site. It includes instructions on setting up a local version of the site, and workflows for building and running the site using Docker or locally. +Local build commands require [Just](https://just.systems/man/en/installation.html) +1.58.0 or newer. Install Just, uv, and Node.js 26, then run: + +```sh +just install-deps +just build-full-website --attack-brand --all-extras +``` + We also have the additional following guides: * A [deployment guide](./test/README.md) for setting up our testing environment diff --git a/attack-search/README.md b/attack-search/README.md new file mode 100644 index 00000000000..10a1b4b592f --- /dev/null +++ b/attack-search/README.md @@ -0,0 +1,29 @@ +# ATT&CK Search + +This project builds the browser search code. The Python search module generates +search JSON separately from the rendered website pages. + +Use Node.js 26 to match Docker. From this directory: + +```sh +npm ci +npm run build-copy +``` + +`npm run build` creates `dist/search_bundle.js`, and `npm run copy` copies it to +`../attack-theme/static/scripts/search_bundle.js`. The copy command creates the +destination directory if needed. It does not update `output/` or existing version +snapshots. `build-copy` runs both commands in order. Use production builds for +committed theme assets. Use `npm run build:dev` for local debugging. + +Git ignores all `dist/` output and `compilation-stats.json`. After changing anything +in `attack-search/`, commit the production bundle in the theme. GitHub Pages uses +the committed bundle without rebuilding it or comparing it with the source. + +The repository build interface uses [Just](../docs/DEVELOPMENT.md). Run +`just build-search` to compile and stage this package. To rebuild both asset sets +and generate the branded website with current-version snapshots, run +`just build-full-website --attack-brand --all-extras`. + +The generated theme `settings.js` is ignored. The developer guide describes its +dataset- and deployment-specific contents. diff --git a/attack-search/__tests__/indexed-db-wrapper.test.js b/attack-search/__tests__/indexed-db-wrapper.test.js index 51e4727b3fc..2b379212209 100644 --- a/attack-search/__tests__/indexed-db-wrapper.test.js +++ b/attack-search/__tests__/indexed-db-wrapper.test.js @@ -57,4 +57,29 @@ describe('IndexedDBWrapper', () => { const count = await contentDb.count(); expect(count).toEqual(data.length); }); + + // A failed write must settle the promise. Racing against a sentinel tells a + // rejection apart from a promise that never settles at all, which a plain + // rejects assertion cannot do: it would time out and look like a slow test. + const settle = (promise) => Promise.race([ + promise.then(() => 'resolved', (error) => `rejected:${error.message}`), + new Promise((resolve) => setTimeout(() => resolve('HUNG'), 1000)), + ]); + + test('Bulk put rejects when the underlying write fails', async () => { + jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut') + .mockRejectedValue(new Error('QuotaExceededError')); + + await expect(settle(contentDb.bulkPut(data))).resolves.toBe('rejected:QuotaExceededError'); + }); + + test('Bulk put rejects when a later chunk fails', async () => { + let calls = 0; + jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut') + .mockImplementation(() => (++calls === 2 + ? Promise.reject(new Error('DatabaseClosedError')) + : Promise.resolve())); + + await expect(settle(contentDb.bulkPut(data, 1))).resolves.toBe('rejected:DatabaseClosedError'); + }); }); diff --git a/attack-search/__tests__/search-events.test.js b/attack-search/__tests__/search-events.test.js index 86af5fc0e19..45d30e8c3c7 100644 --- a/attack-search/__tests__/search-events.test.js +++ b/attack-search/__tests__/search-events.test.js @@ -112,8 +112,90 @@ describe('search event bindings', () => { expect(mockJqueryApis['[data-search-filter-dropdown="core"]'].attr) .toHaveBeenCalledWith('aria-hidden', 'false'); }); + + test('a failed index build stops search from waiting for an index that never arrives', async () => { + await loadIndexWithAFailingColdStart(); + + const parsingIcon = mockJqueryApis['#search-parsing-icon']; + parsingIcon.show.mockClear(); + parsingIcon.hide.mockClear(); + + handlerForSelector('#search-input')({ target: { value: 'mimikatz' } }); + + // Before the fix `search` looped on the loaded flag, so it showed the parsing icon on + // its first pass and kept doing so every 100ms for as long as the page stayed open. + expect(parsingIcon.show).not.toHaveBeenCalled(); + expect(parsingIcon.hide).toHaveBeenCalled(); + }); + + test('a failed restore from the cache is not reported as a successful load', async () => { + const { cacheKey, deleteCachedDatabase } = await loadIndexWithAFailingWarmRestore(); + + // The catch used to set the loaded flag false and the finally set it straight back to + // true, so `search` went on to query an index that was never populated. + expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon'); + expect(global.localStorage.removeItem).toHaveBeenCalledWith(cacheKey); + expect(deleteCachedDatabase).toHaveBeenCalledTimes(1); + }); + + test('a failed index build puts the search controls into their unavailable state', async () => { + await loadIndexWithAFailingColdStart(); + + expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-button'].prop).toHaveBeenCalledWith('disabled', true); + expect(mockJqueryApis['#search-icon'].removeClass).toHaveBeenCalledWith('search-icon'); + expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon'); + expect(mockJqueryApis['#search-button'].prop) + .toHaveBeenCalledWith('title', expect.stringContaining('search index could not be built')); + }); }); +// Load the module on the cold-start path with the document fetch failing, and run the +// debouncer straight through so the input handler reaches `search` without a timer. +async function loadIndexWithAFailingColdStart() { + global.window = { indexedDB: {} }; + global.localStorage.getItem.mockReturnValue(null); + + jest.doMock('../src/search-loader.js', () => ({ + loadSearchDocuments: () => Promise.reject(new Error('documents unavailable')), + })); + jest.doMock('../src/debouncer.js', () => class { + debounce(callback) { + callback(); + } + }); + + require('../src/index'); + await new Promise(resolve => setImmediate(resolve)); +} + +// Load the module on the cached path, with restoring the index from IndexedDB failing. +async function loadIndexWithAFailingWarmRestore() { + const { searchCacheCompatibilityVersion, searchCacheSchemaVersion } = require('../src/settings'); + const version = `${searchCacheSchemaVersion}-${searchCacheCompatibilityVersion}`; + const cacheKey = `saved_uuid_search_schema_${version}`; + const deleteCachedDatabase = jest.fn(() => Promise.resolve()); + + global.window = { indexedDB: {} }; + global.localStorage.getItem.mockReturnValue(`${global.build_uuid}-search-${version}`); + + jest.doMock('../src/search-service.js', () => class { + constructor() { + this.db = { indexeddb: { delete: deleteCachedDatabase } }; + } + + initializeAsync() { + return Promise.reject(new Error('cached index is unreadable')); + } + }); + + require('../src/index'); + await new Promise(resolve => setImmediate(resolve)); + + return { cacheKey, deleteCachedDatabase }; +} + function eventsForSelector(selector) { return mockJqueryCalls .filter(call => call.selector === selector || call.delegatedSelector === selector) diff --git a/attack-search/__tests__/search-service.test.js b/attack-search/__tests__/search-service.test.js index 87beddcec06..5c5c1c91771 100644 --- a/attack-search/__tests__/search-service.test.js +++ b/attack-search/__tests__/search-service.test.js @@ -22,11 +22,17 @@ describe('SearchService', () => { }); beforeEach(() => { + global.base_url = '/'; searchService = new SearchService('search-service', null); + searchService.render_container = { + append: jest.fn(), + html: jest.fn(), + }; }); afterEach(async () => { searchService = null; + delete global.base_url; }); it('Access data from mock-index.json', () => { @@ -111,4 +117,117 @@ describe('SearchService', () => { }); }); + + test('Keeps only exact ATT&CK ID matches and references, with the object first', async () => { + const documents = { + 1: { + id: 1, + title: 'TA577, Group G1037', + path: '/groups/G1037/index.html', + content: 'A group with no reference to the queried technique.', + attackId: 'G1037', + }, + 2: { + id: 2, + title: 'Ingress Tool Transfer, Technique T1105 - Enterprise', + path: '/techniques/T1105/index.html', + content: 'The T1105 technique.', + attackId: 'T1105', + }, + 3: { + id: 3, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references T1105.', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2, 3] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('t1105'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 3]); + }); + + test('Treats a four-digit query as an exact ATT&CK ID suffix search', async () => { + const documents = { + 1: { + id: 1, + title: 'TA577, Group G1037', + path: '/groups/G1037/index.html', + content: 'A group with no reference to the queried technique.', + attackId: 'G1037', + }, + 2: { + id: 2, + title: 'Data from Local System, Technique T1005 - Enterprise', + path: '/techniques/T1005/index.html', + content: 'The T1005 technique.', + attackId: 'T1005', + }, + 3: { + id: 3, + title: 'Matching software, Software S1005', + path: '/software/S1005/index.html', + content: 'The S1005 software.', + attackId: 'S1005', + }, + 4: { + id: 4, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references T1005.', + }, + 5: { + id: 5, + title: 'Data from Local System: Archive Collected Data, Sub-technique T1005.001', + path: '/techniques/T1005/001/index.html', + content: 'The T1005.001 sub-technique.', + attackId: 'T1005.001', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 3, 4, 2, 5] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('1005'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 5, 3, 4]); + }); + + test.each(['TA0001', '0001'])('Promotes a tactic page for ATT&CK ID query %s', async (query) => { + const documents = { + 1: { + id: 1, + title: 'A valid reference', + path: '/resources/reference/index.html', + content: 'This page references TA0001.', + }, + 2: { + id: 2, + title: 'Initial Access, Tactic TA0001 - Enterprise', + path: '/tactics/TA0001/index.html', + content: 'The TA0001 tactic.', + attackId: 'TA0001', + }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query(query); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([2, 1]); + }); + + test('Preserves result ordering for non-ID queries', async () => { + const documents = { + 1: { id: 1, title: 'First result', path: '/resources/faq/index.html', content: 'Resources' }, + 2: { id: 2, title: 'Second result', path: '/resources/attackcon/index.html', content: 'Resources' }, + }; + searchService.attackIndex.search = jest.fn().mockResolvedValue([{ field: 'title', result: [1, 2] }]); + searchService.resolveSearchResults = jest.fn(async positions => positions.map(position => documents[position])); + + await searchService.query('Resources'); + + expect(searchService.allSearchResults.map(result => result.id)).toEqual([1, 2]); + }); }); diff --git a/attack-search/__tests__/search-style.test.js b/attack-search/__tests__/search-style.test.js index 73a43d2718e..f5713431710 100644 --- a/attack-search/__tests__/search-style.test.js +++ b/attack-search/__tests__/search-style.test.js @@ -20,7 +20,7 @@ describe('search styles', () => { const badgeStyle = styles.match(/\.search-result-badge\s*\{(?[^}]+)\}/)?.groups?.body ?? ''; - expect(badgeStyle).toContain('color: white;'); + expect(badgeStyle).toContain('color: color-functions.on-color(active);'); expect(badgeStyle).toContain('font-size: 0.8rem;'); expect(styles).toContain('.search-result-badge-page-type'); diff --git a/attack-search/__tests__/theme.test.js b/attack-search/__tests__/theme.test.js new file mode 100644 index 00000000000..ea6325bf4d3 --- /dev/null +++ b/attack-search/__tests__/theme.test.js @@ -0,0 +1,403 @@ +const fs = require('fs'); +const path = require('path'); + +const themeModulePath = '../../attack-theme/static/scripts/theme.js'; + +describe('site theme', () => { + let theme; + + beforeEach(() => { + jest.resetModules(); + theme = require(themeModulePath); + }); + + test('uses System when no saved override exists', () => { + const storage = createStorage(); + const root = createRoot(); + + expect(theme.readStoredPreference(storage)).toBe('system'); + + theme.applyPreference(root, 'system'); + + expect(root.removeAttribute).toHaveBeenCalledWith('data-theme'); + expect(root.style.setProperty).not.toHaveBeenCalled(); + }); + + test.each(['light', 'dark'])('applies a saved %s override before controls initialize', preference => { + const storage = createStorage(preference); + const root = createRoot(); + + const storedPreference = theme.readStoredPreference(storage); + theme.applyPreference(root, storedPreference); + + expect(storedPreference).toBe(preference); + expect(root.setAttribute).toHaveBeenCalledWith('data-theme', preference); + // CSS owns color-scheme so the print stylesheet can force light controls. + expect(root.style.setProperty).not.toHaveBeenCalled(); + }); + + test('discards an invalid saved preference', () => { + const storage = createStorage('sepia'); + + expect(theme.readStoredPreference(storage)).toBe('system'); + expect(storage.removeItem).toHaveBeenCalledWith(theme.STORAGE_KEY); + }); + + test('adds a working archive switch to the existing banner without replacing its content', () => { + const fixture = createControllerFixture({ storedPreference: 'dark' }); + const banner = { appendChild: jest.fn(), textContent: 'Currently viewing ATT&CK v3.0' }; + fixture.document.querySelector = jest.fn(selector => ( + selector === '.version-banner' ? banner : null + )); + fixture.document.createElement = jest.fn(() => fixture.toggle); + fixture.document.readyState = 'complete'; + + theme.bootstrap({ ...fixture, archived: true }); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenCalledWith('data-archive-theme', ''); + expect(banner.appendChild).toHaveBeenCalledWith(fixture.toggle); + expect(banner.textContent).toBe('Currently viewing ATT&CK v3.0'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Dark mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + fixture.toggle.click(); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + }); + + test('toggles from the system light theme to a saved dark override', () => { + const fixture = createControllerFixture({ systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'dark'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to light mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + }); + + test('toggles from the system dark theme to a saved light override', () => { + const fixture = createControllerFixture({ systemDark: true }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'light'); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + }); + + test.each([ + ['light', 'dark'], + ['dark', 'light'], + ])('toggles a saved %s override to %s', (storedPreference, expectedPreference) => { + const fixture = createControllerFixture({ storedPreference }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.toggle.click(); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith( + 'data-theme', + expectedPreference, + ); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, expectedPreference); + }); + + test('handles toggle clicks before DOMContentLoaded without double toggling after initialization', () => { + const fixture = createControllerFixture({ storedPreference: 'dark' }); + fixture.document.readyState = 'loading'; + + theme.bootstrap(fixture); + + // The stored preference is still applied synchronously to prevent a light-theme flash. + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + + fixture.document.dispatchClick({ closest: jest.fn(() => fixture.toggle) }); + + expect(fixture.storage.setItem).toHaveBeenLastCalledWith(theme.STORAGE_KEY, 'light'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + + fixture.document.dispatchDOMContentLoaded(); + fixture.toggle.click(); + + expect(fixture.storage.setItem).toHaveBeenLastCalledWith(theme.STORAGE_KEY, 'dark'); + expect(fixture.storage.setItem).toHaveBeenCalledTimes(2); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + }); + + test('ignores delegated clicks outside the theme toggle', () => { + const fixture = createControllerFixture(); + const controller = theme.createThemeController(fixture); + + fixture.document.dispatchClick({ closest: jest.fn(() => null) }); + controller.init(); + + expect(fixture.storage.setItem).not.toHaveBeenCalled(); + }); + + test('continues applying a choice when storage access fails', () => { + const fixture = createControllerFixture(); + fixture.storage.setItem.mockImplementation(() => { + throw new Error('Storage disabled'); + }); + const controller = theme.createThemeController(fixture); + + expect(() => { + controller.init(); + fixture.toggle.click(); + }).not.toThrow(); + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + }); + + test('updates the toggle when the system preference changes before an override', () => { + const fixture = createControllerFixture({ systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to light mode'); + }); + + test('ignores OS preference changes while an explicit override is active', () => { + const fixture = createControllerFixture({ storedPreference: 'light', systemDark: false }); + const controller = theme.createThemeController(fixture); + + controller.init(); + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-label', 'Switch to dark mode'); + }); + + test('synchronizes a theme change from another tab without writing it back', () => { + const fixture = createControllerFixture({ storedPreference: 'light' }); + theme.createThemeController(fixture).init(); + + fixture.storage.getItem.mockReturnValue('dark'); + fixture.document.defaultView.dispatchStorage({ + key: theme.STORAGE_KEY, + storageArea: fixture.storage, + }); + + expect(fixture.document.documentElement.setAttribute).toHaveBeenLastCalledWith('data-theme', 'dark'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'true'); + expect(fixture.storage.setItem).not.toHaveBeenCalled(); + fixture.toggle.click(); + expect(fixture.storage.setItem).toHaveBeenCalledWith(theme.STORAGE_KEY, 'light'); + }); + + test.each([null, 'attack-website-theme'])('returns to the system theme after storage removal (%s)', key => { + const fixture = createControllerFixture({ storedPreference: 'dark', systemDark: false }); + theme.createThemeController(fixture).init(); + + fixture.storage.getItem.mockReturnValue(null); + fixture.document.defaultView.dispatchStorage({ key, storageArea: fixture.storage }); + + expect(fixture.document.documentElement.removeAttribute).toHaveBeenCalledWith('data-theme'); + expect(fixture.toggle.setAttribute).toHaveBeenCalledWith('aria-checked', 'false'); + fixture.mediaQuery.matches = true; + fixture.mediaQuery.dispatchChange(); + expect(fixture.toggle.setAttribute).toHaveBeenLastCalledWith('data-theme-effective', 'dark'); + }); + + test('ignores unrelated storage keys and storage areas', () => { + const fixture = createControllerFixture({ storedPreference: 'light' }); + theme.createThemeController(fixture).init(); + fixture.storage.getItem.mockClear(); + + fixture.document.defaultView.dispatchStorage({ key: 'other', storageArea: fixture.storage }); + fixture.document.defaultView.dispatchStorage({ key: theme.STORAGE_KEY, storageArea: createStorage('dark') }); + + expect(fixture.storage.getItem).not.toHaveBeenCalled(); + }); + + test('loads the early theme script before styles and renders one toggle before search', () => { + const template = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/general/base-template.html'), + 'utf8', + ); + const navigation = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/macros/navigation_menu.html'), + 'utf8', + ); + + expect(template).toContain(''); + expect(template.indexOf('/theme/scripts/theme.js')).toBeLessThan(template.indexOf('bootstrap.min.css')); + expect(navigation.match(/\bdata-theme-toggle(?=[\s>])/g)).toHaveLength(1); + expect(navigation.indexOf('id="theme-toggle"')).toBeLessThan(navigation.indexOf('id="search-button"')); + expect(navigation).toContain('role="switch"'); + expect(navigation).toContain('class="theme-toggle-track"'); + expect(navigation).toContain('class="theme-toggle-thumb"'); + expect(navigation).toContain('aria-checked="false"'); + expect(navigation).not.toContain('aria-pressed'); + expect(navigation).not.toContain('data-theme-option'); + expect(navigation).not.toContain('theme-menu'); + expect(navigation).not.toContain('dropdown-toggle" type="button" data-theme-toggle'); + }); + + test('uses theme-aware surfaces for the affected resource pages', () => { + const council = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attack-advisory-council-members.html'), + 'utf8', + ); + const dataTools = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attack-data-and-tools.html'), + 'utf8', + ); + const attackcon = fs.readFileSync( + path.join(__dirname, '../../modules/resources/templates/attackcon-overview.html'), + 'utf8', + ); + + expect(council).toContain('background: var(--attack-color-body-alternate);'); + expect(council).toContain('color: var(--attack-on-color-body);'); + expect(dataTools).toContain('class="tab-content card card-body p-3 attack-excel-files"'); + expect(dataTools).not.toContain('style="background: #f8f9fa;"'); + expect(attackcon).toContain('"ATT&CKcon 4.0", "ATT&CKcon 5.0", "ATT&CKcon 6.0", "ATT&CKcon 7.0"'); + expect(attackcon).toContain('attackcon-banner-image{% if con.title in light_banner_titles %} on-light{% endif %}'); + }); + + test('uses theme-aware home controls and announcement banner colors', () => { + const home = fs.readFileSync( + path.join(__dirname, '../../attack-theme/templates/general/attack-index.html'), + 'utf8', + ); + const colors = fs.readFileSync( + path.join(__dirname, '../../attack-style/themes/_palette.scss'), + 'utf8', + ); + + expect(home).toContain('fa-up-right-from-square external-link-icon'); + expect(home).toContain('dropdown-toggle-split random-page-toggle'); + expect(home).not.toContain('external-site-dark.jpeg'); + expect(home).not.toContain('style="color: #4f7cac; background-color: white;'); + expect(colors).toContain('--attack-color-banner: #e7f0f6;'); + expect(colors).toContain('--attack-color-banner: #263a49;'); + }); + + test('renders the matrix Navigator link with a theme-aware icon', () => { + const matrix = fs.readFileSync( + path.join(__dirname, '../../modules/matrices/templates/matrix.html'), + 'utf8', + ); + + expect(matrix).toContain('fa-up-right-from-square'); + expect(matrix).not.toContain('external-site-dark.jpeg'); + }); + + test('paints the initial toggle state from the root theme and suppresses the Bootstrap focus ring', () => { + const nav = fs.readFileSync( + path.join(__dirname, '../../attack-style/layout/_nav.scss'), + 'utf8', + ); + + expect(nav).toContain(':root[data-theme="dark"] &'); + expect(nav).toContain(':root:not([data-theme]) &'); + expect(nav).toMatch(/&:focus\s*\{\s*outline: 0;\s*box-shadow: none;/); + }); + + test('uses a warm metadata label color only in dark mode', () => { + const colors = fs.readFileSync( + path.join(__dirname, '../../attack-style/themes/_palette.scss'), + 'utf8', + ); + const layout = fs.readFileSync( + path.join(__dirname, '../../attack-style/layout/_layout.scss'), + 'utf8', + ); + + expect(colors).toContain('--attack-color-property-label: #1d2226;'); + expect(colors).toContain('--attack-color-property-label: #f2d2a4;'); + expect(layout).toMatch(/\.card-data \.card-title\s*\{\s*color: color-functions\.color\(property-label\);/); + }); +}); + +function createStorage(value = null) { + return { + getItem: jest.fn(() => value), + removeItem: jest.fn(), + setItem: jest.fn(), + }; +} + +function createRoot() { + return { + removeAttribute: jest.fn(), + setAttribute: jest.fn(), + style: { + removeProperty: jest.fn(), + setProperty: jest.fn(), + }, + }; +} + +function createElement() { + const listeners = {}; + let dispatchClick; + const element = { + classList: { toggle: jest.fn() }, + setAttribute: jest.fn(), + addEventListener: jest.fn((eventName, listener) => { + listeners[eventName] = listener; + }), + closest: jest.fn(selector => (selector === '[data-theme-toggle]' ? element : null)), + connectClickDispatcher: dispatcher => { + dispatchClick = dispatcher; + }, + click: () => { + const event = { preventDefault: jest.fn(), target: element }; + if (listeners.click) listeners.click(event); + if (dispatchClick) dispatchClick(event); + }, + }; + + return element; +} + +function createControllerFixture({ storedPreference = null, systemDark = false } = {}) { + const root = createRoot(); + const toggle = createElement(); + let changeListener; + let clickListener; + let domContentLoadedListener; + let storageListener; + const mediaQuery = { + matches: systemDark, + addEventListener: jest.fn((eventName, listener) => { + if (eventName === 'change') changeListener = listener; + }), + dispatchChange: () => changeListener({ matches: mediaQuery.matches }), + }; + const document = { + documentElement: root, + querySelector: jest.fn(selector => (selector === '[data-theme-toggle]' ? toggle : null)), + addEventListener: jest.fn((eventName, listener) => { + if (eventName === 'click') clickListener = listener; + if (eventName === 'DOMContentLoaded') domContentLoadedListener = listener; + }), + dispatchClick: target => clickListener({ preventDefault: jest.fn(), target }), + dispatchDOMContentLoaded: () => domContentLoadedListener(), + defaultView: { + addEventListener: jest.fn((eventName, listener) => { + if (eventName === 'storage') storageListener = listener; + }), + dispatchStorage: event => storageListener(event), + }, + }; + toggle.connectClickDispatcher(event => clickListener(event)); + + return { + document, + mediaQuery, + storage: createStorage(storedPreference), + toggle, + }; +} diff --git a/attack-search/package.json b/attack-search/package.json index f1ec9bd68a7..1c772fe4b22 100644 --- a/attack-search/package.json +++ b/attack-search/package.json @@ -8,7 +8,8 @@ "babel": "babel src -d dist", "build": "webpack --mode production", "build:dev": "webpack --mode development", - "copy": "rm -f ../output/theme/scripts/search_bundle.js && cp dist/search_bundle.js ../output/theme/scripts", + "copy": "mkdir -p ../attack-theme/static/scripts && cp dist/search_bundle.js ../attack-theme/static/scripts/search_bundle.js", + "build-copy": "npm run build && npm run copy", "dev": "webpack-dev-server --mode development --json=compilation-stats.json", "lint": "eslint src", "lint:fix": "eslint --fix src", diff --git a/attack-search/src/index.js b/attack-search/src/index.js index d3aa49f3b30..54ad023b402 100644 --- a/attack-search/src/index.js +++ b/attack-search/src/index.js @@ -89,6 +89,40 @@ const closeSearch = function () { // Variable to check if search service is loaded let searchServiceIsLoaded = false; +// Set once the index cannot be built at all. Without it `search` waits for a flag that is +// never going to flip and the parsing spinner runs for as long as the page is open. +let searchServiceUnavailable = false; + +// Put the search controls into their unavailable state and explain why on hover. +function markSearchUnavailable(reason) { + searchServiceUnavailable = true; + searchServiceIsLoaded = false; + searchInput.prop('disabled', true); + searchButton.prop('disabled', true); + searchIcon.removeClass('search-icon'); + searchIcon.addClass('error-icon'); + searchButton.prop('title', reason); +} + +// Remove a failed cached index so the next page load rebuilds it instead of retrying +// the same restore path. Cache cleanup is best-effort and must not mask the original +// initialization failure or prevent the unavailable UI state from being shown. +async function invalidateSearchCache() { + try { + localStorage.removeItem(searchCacheKey); + } catch (error) { + console.error('Failed to remove the search cache marker:', error); + } + + try { + await searchService.db.indexeddb.delete(); + } catch (error) { + console.error('Failed to delete the cached search index:', error); + } +} + +const SEARCH_INDEX_FAILED_MESSAGE = 'The search index could not be built. Reload the page to try again.'; + // Initialize the search service async function initializeSearchService() { console.debug('Initializing search service...'); @@ -111,12 +145,13 @@ async function initializeSearchService() { await searchService.initializeAsync(null); // Passing null will instruct the search service to attempt // restoring itself from the IndexedDB console.debug('SearchService is initialized.'); + searchServiceIsLoaded = true; } catch (error) { console.error('Failed to initialize SearchService:', error); - searchServiceIsLoaded = false; + markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE); + await invalidateSearchCache(); } finally { searchParsingIcon.hide(); - searchServiceIsLoaded = true; } } else { @@ -139,18 +174,14 @@ async function initializeSearchService() { .catch(error => { console.error('Failed to initialize SearchService:', error); searchParsingIcon.hide(); - searchServiceIsLoaded = false; + markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE); }); } } else { // Disable the search button and display an error icon with a hover effect that displays a message/explanation console.error('Search is only available in browsers that support IndexedDB. Please try using Firefox, Chrome, Safari, or another browser that supports IndexedDB.'); - searchInput.prop('disabled', true); - searchButton.prop('disabled', true); - searchIcon.removeClass('search-icon'); - searchIcon.addClass('error-icon'); - searchButton.prop('title', 'To use the search feature, please make sure your browser supports IndexedDB. If not, consider upgrading your browser or switching to a supported browser such as Firefox, Chrome, or Safari.') + markSearchUnavailable('To use the search feature, please make sure your browser supports IndexedDB. If not, consider upgrading your browser or switching to a supported browser such as Firefox, Chrome, or Safari.'); } } @@ -158,13 +189,18 @@ async function initializeSearchService() { const search = async function (query) { console.debug(`search -> Received search query: ${query}`); - // Wait until the search service is loaded - while (!searchServiceIsLoaded) { + // Wait until the search service is loaded, or until we know it never will be. + while (!searchServiceIsLoaded && !searchServiceUnavailable) { console.debug('search -> search index is not loaded...'); searchParsingIcon.show(); await new Promise(resolve => setTimeout(resolve, 100)); } + if (searchServiceUnavailable) { + searchParsingIcon.hide(); + return; + } + console.debug(`Executing search: ${query}`); await searchService.query(query); searchParsingIcon.hide(); diff --git a/attack-search/src/indexed-db-wrapper.js b/attack-search/src/indexed-db-wrapper.js index df24f3ff08b..9ac1400b02a 100644 --- a/attack-search/src/indexed-db-wrapper.js +++ b/attack-search/src/indexed-db-wrapper.js @@ -23,7 +23,7 @@ class TableWrapper { */ async bulkPut(data, chunkSize = 100) { - return new Promise(async (resolve) => { + return new Promise((resolve, reject) => { /** * Schedules work using requestIdleCallback if supported, or setTimeout as a fallback. * @param {Function} callback - The function to be executed when the browser is idle or after the specified delay. @@ -44,23 +44,28 @@ class TableWrapper { * @param {number} start - The index of the first item in the data array to be included in the current chunk. */ const putChunk = async (start) => { - // If all data has been processed, resolve the promise - if (start >= data.length) { - resolve(); - return; + try { + // If all data has been processed, resolve the promise + if (start >= data.length) { + resolve(); + return; + } + + // Determine the end index for the current chunk + const end = Math.min(start + chunkSize, data.length); + + // Extract the chunk from the data array + const chunk = data.slice(start, end); + + // Insert the chunk into the IndexedDB table + await this.indexeddb[this.tableName].bulkPut(chunk); + + // Schedule the next chunk to be processed + scheduleWork(() => putChunk(end)); + } catch (error) { + // Nothing else settles this promise, so callers would wait forever. + reject(error); } - - // Determine the end index for the current chunk - const end = Math.min(start + chunkSize, data.length); - - // Extract the chunk from the data array - const chunk = data.slice(start, end); - - // Insert the chunk into the IndexedDB table - await this.indexeddb[this.tableName].bulkPut(chunk); - - // Schedule the next chunk to be processed - scheduleWork(() => putChunk(end)); }; // Start processing the data array by inserting the first chunk diff --git a/attack-search/src/search-service.js b/attack-search/src/search-service.js index 5a38f6ecfe5..bd03b763296 100644 --- a/attack-search/src/search-service.js +++ b/attack-search/src/search-service.js @@ -313,10 +313,84 @@ module.exports = class SearchService { * ] */ - this.allSearchResults = await this.#setSearchResults(results); + this.allSearchResults = this.#filterAndPromoteExactAttackIdMatches(await this.#setSearchResults(results)); this.#renderFilteredSearchResults(); } + /** + * Limits ATT&CK ID searches to matching objects, their sub-techniques, and genuine references. + * Non-ID and multi-token queries retain FlexSearch's existing ordering. + * + * @private + * @param {Array} documents - Search results in their existing relevance order. + * @returns {Array} Exact ATT&CK ID results, with the matching object detail page first when applicable. + */ + #filterAndPromoteExactAttackIdMatches(documents) { + const query = this.currentQuery.clean; + const isExactAttackId = /^[A-Z]+\d+(?:\.\d+)?$/i.test(query); + const isNumericIdSuffix = /^\d{4}$/.test(query); + // If user queries for normal text and not attack ids, normal search takes place + if (!isExactAttackId && !isNumericIdSuffix) return documents; + + const normalizedQuery = query.toUpperCase(); + + // Collect the IDs stored on object-detail search records. Resource and reference pages have no attackId. + const candidateAttackIds = documents + .map(document => document.attackId?.toUpperCase()) + .filter(Boolean); + + let directAttackIds; + if (isExactAttackId) { + // A complete query such as T1005 refers directly to that one ID. + directAttackIds = [normalizedQuery]; + } else { + // A numeric query such as 1005 may match T1005, S1005, or another complete ATT&CK ID. + const numericSuffixPattern = new RegExp(`^[A-Z]+${normalizedQuery}$`); + directAttackIds = [...new Set(candidateAttackIds.filter(attackId => numericSuffixPattern.test(attackId)))]; + } + + // Include sub-techniques of a matching parent technique, such as T1005.001 for a T1005 query. + const subTechniqueIds = candidateAttackIds.filter((attackId) => directAttackIds.some((directAttackId) => ( + directAttackId.startsWith('T') + && !directAttackId.includes('.') + && attackId.startsWith(`${directAttackId}.`) + ))); + const matchingAttackIds = [...new Set([...directAttackIds, ...subTechniqueIds])]; + if (matchingAttackIds.length === 0) return []; + + // Put parent techniques first, then their sub-techniques, followed by other matching ATT&CK object types. + const exactMatches = documents.filter(document => matchingAttackIds.includes(document.attackId?.toUpperCase())); + exactMatches.sort((first, second) => { + const firstIsTechnique = first.attackId.startsWith('T'); + const secondIsTechnique = second.attackId.startsWith('T'); + if (firstIsTechnique !== secondIsTechnique) return firstIsTechnique ? -1 : 1; + + const firstIsSubTechnique = first.attackId.includes('.'); + const secondIsSubTechnique = second.attackId.includes('.'); + if (firstIsSubTechnique !== secondIsSubTechnique) return firstIsSubTechnique ? 1 : -1; + + return 0; + }); + + // Escape dots in sub-technique IDs before making one expression that matches only whole IDs. + const escapedIds = matchingAttackIds.map(attackId => attackId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + // A trailing period is valid sentence punctuation, unless it begins a sub-technique suffix such as .001. + const exactIdInText = new RegExp( + `(^|[^A-Z0-9.])(?:${escapedIds.join('|')})(?=$|[^A-Z0-9.]|\\.(?!\\d))`, + 'i', + ); + + // Add pages that reference a matching ID, but do not add an object-detail page twice. + const referencedDocuments = documents.filter((document) => { + const title = document.title ?? ''; + const content = document.content ?? ''; + const referencesMatchingId = exactIdInText.test(title) || exactIdInText.test(content); + return referencesMatchingId && !exactMatches.includes(document); + }); + + return exactMatches.concat(referencedDocuments); + } + /** * Renders the search results on the web page based on the given search result page. * If the search query is empty, it will show the "Load More Results" button. diff --git a/attack-search/src/settings.js b/attack-search/src/settings.js index 14451cd250b..6218aace463 100644 --- a/attack-search/src/settings.js +++ b/attack-search/src/settings.js @@ -1,7 +1,7 @@ const baseURL = ''; // TODO migrate from base_url (generated via Pelican) const packageJson = require('../package.json'); -const searchCacheSchemaVersion = 3; +const searchCacheSchemaVersion = 4; const flexSearchVersion = packageJson.dependencies.flexsearch.replace(/^[^\d]*/, ''); const searchCacheCompatibilityVersion = `flexsearch-${flexSearchVersion}`; diff --git a/attack-style/README.md b/attack-style/README.md index 201a7c83507..5bb7c67d058 100644 --- a/attack-style/README.md +++ b/attack-style/README.md @@ -1,14 +1,21 @@ # ATT&CK Style ATT&CK Style is a JavaScript package that builds the CSS styles for the ATT&CK website. -The outputs are simply 2 CSS files: +The outputs are 3 CSS files: * `dist/style-attack.css` * `dist/style-user.css` +* `dist/style-archive.css` (preserved-site appearance compatibility) These files are then copied into `/attack-theme/static/`. -Currently this is done manually - no automation. -But also, the CSS is not updated very often. +Use `npm run build-copy` to compile and copy all three files, or use the repository's +[build commands](../docs/DEVELOPMENT.md#commands-and-generator-options) to build the complete website. +Intermediate `dist/` outputs are ignored; the three compiled CSS files in the theme +must be regenerated and committed after changing anything in `attack-style/`, just +like the compiled ATT&CK Search bundle after changes to `attack-search/`. GitHub Pages +uses the committed assets without rebuilding or comparing them against source. +The repository build interface requires [Just](../docs/DEVELOPMENT.md); use +`just build-style` to compile and stage this package or `just build-assets` for both. ## Installation @@ -16,7 +23,7 @@ To set up the ATT&CK Style package, follow these steps: 1. **Prerequisite: Ensure Node.js is Installed**: - Make sure you have the latest Node.js LTS version installed. + Use Node.js 26 to match Docker. 2. **Navigate to the attack-style Sub-folder**: @@ -31,7 +38,7 @@ To set up the ATT&CK Style package, follow these steps: Run the following command to install the necessary dependencies: ```bash - npm install + npm ci ``` ## Build @@ -47,7 +54,7 @@ To set up the ATT&CK Style package, follow these steps: 2. **Copy CSS Files**: - Copy both `dist/style-attack.css` and `dist/style-user.css` to `/attack-theme/static/`. + Copy `dist/style-attack.css`, `dist/style-user.css`, and `dist/style-archive.css` to `/attack-theme/static/`. ```bash npm run copy diff --git a/attack-style/abstracts/README.md b/attack-style/abstracts/README.md index c6327328313..c354b0af464 100644 --- a/attack-style/abstracts/README.md +++ b/attack-style/abstracts/README.md @@ -8,14 +8,14 @@ Files in this folder should not emit large blocks of CSS on their own unless the | File | Purpose | | --- | --- | | `_variables.scss` | Defines brand and user color maps plus the semantic `$colors` map used across the site. | -| `_color-functions.scss` | Provides accessors and derived color helpers for entries in `$colors`. | +| `_color-functions.scss` | Provides accessors for runtime semantic color tokens. | | `_utilities.scss` | Provides small reusable mixins and unit helpers. | | `_font-faces.scss` | Defines shared font-face declarations. | ## Color Model -`_variables.scss` keeps raw brand values separate from semantic color names. -Most styles should use semantic keys from `$colors`, such as `primary`, `secondary`, `footer`, `active`, `body`, `link`, `matrix-header`, `search-highlight`, and `deemphasis`. +`_variables.scss` keeps raw brand values separate from semantic color names. The theme palette in `themes/_palette.scss` turns those values into CSS custom properties so the appearance can change without loading another stylesheet. +Most styles should use semantic names such as `primary`, `secondary`, `footer`, `active`, `body`, `link`, `matrix-header`, `search-highlight`, and `deemphasis`. Each color entry may contain: @@ -28,18 +28,17 @@ Some entries omit `on-color` when they are not meant to contain inner text. ## Helper Functions -Use the functions in `_color-functions.scss` instead of reading `$colors` directly from component or layout files: +Use the functions in `_color-functions.scss` instead of reading `$colors` directly from component or layout files. Each helper returns the appropriate runtime CSS custom property: | Function | Use | | --- | --- | | `color($name)` | Reads the base color for a semantic color name. | -| `on-color($name)` | Reads the readable text color for a semantic color name. | -| `color-alternate($name, $contrast: 1)` | Computes a nearby alternate shade for patterning or subtle contrast. | -| `on-color-emphasis($name)` | Computes a stronger foreground color against a semantic background. | -| `on-color-deemphasis($name)` | Computes a quieter foreground color against a semantic background. | -| `border-color($name)` | Computes a border color for a semantic background. | -| `background-color($name)` | Computes a subtle derived background shade. | -| `escape-color($color)` | Escapes a concrete color for use inside inline SVG data URLs. | +| `on-color($name)` | Reads the readable foreground for a semantic color name. | +| `color-alternate($name, $contrast: 1)` | Reads an explicit alternate surface token. Supported contrast levels are `0.8`, `1`, `1.5`, `2`, and `3`. | +| `on-color-emphasis($name)` | Reads a stronger foreground token. | +| `on-color-deemphasis($name)` | Reads a quieter foreground token. | +| `border-color($name)` | Reads a border token. | +| `background-color($name)` | Reads a related background token. | ## Utility Mixins And Functions diff --git a/attack-style/abstracts/_color-functions.scss b/attack-style/abstracts/_color-functions.scss index 4407f1f3d8b..69b0b1dc9c7 100644 --- a/attack-style/abstracts/_color-functions.scss +++ b/attack-style/abstracts/_color-functions.scss @@ -1,55 +1,55 @@ -@use "sass:color"; -@use "sass:map"; -@use "sass:string"; -@use "variables"; - -// accessor helper for $colors. Gets the color of the named pair +// Accessor helper for semantic runtime colors. @function color($name) { - @return map.get(map.get(variables.$colors, $name), "color"); + @return var(--attack-color-#{$name}); } -// given a color name, get an alternate version of the color, for patterning -// if the base color is dark, the alternate will be slightly lighter. -// if the base color is light, the alternate will be slightly darker. -// contrast, an optional argument, multiplies to create a more distint or similar color. >1 is more distant, <1 is more similar. +// Get an explicit alternate surface token. Supported contrast values match the +// existing call sites and avoid requiring runtime color-mix support. @function color-alternate($name, $contrast: 1) { - @return color.mix(color.invert(color($name)), color($name), $weight: $contrast * 5%); + @if $contrast == 0.8 { + @return var(--attack-color-#{$name}-alternate-subtle); + } + + @if $contrast == 1 { + @return var(--attack-color-#{$name}-alternate); + } + + @if $contrast == 1.5 { + @return var(--attack-color-#{$name}-alternate-medium); + } + + @if $contrast == 2 { + @return var(--attack-color-#{$name}-alternate-strong); + } + + @if $contrast == 3 { + @return var(--attack-color-#{$name}-alternate-strongest); + } + + @error "Unsupported alternate color contrast: #{$contrast}"; } -/// accessor helper for $colors. Gets the on-color of the named pair +/// Accessor helper for readable text on a semantic color. @function on-color($name) { - @return map.get(map.get(variables.$colors, $name), "on-color"); + @return var(--attack-on-color-#{$name}); } -/// given a color-name, get an emphasized version of the on-color. -/// The emphasized on-color is less like the background color. +/// Get an emphasized foreground token for a semantic color. @function on-color-emphasis($name) { - @return color.mix(color.invert(color($name)), on-color($name)); + @return var(--attack-on-color-#{$name}-emphasis); } -// given a color-name, get an deemphasized version of the on-color. -// The deemphasized on-color is more like the background color. +// Get a deemphasized foreground token for a semantic color. @function on-color-deemphasis($name) { - @return color.mix(color($name), on-color($name), 25%); + @return var(--attack-on-color-#{$name}-deemphasis); } -// given a color name, compute a border color for the color +// Get an explicit border token for a semantic color. @function border-color($name) { - @return color.mix(color.invert(color($name)), color($name), 12.5%); - - // @return rgba(invert(color($name)), 0.125); + @return var(--attack-border-color-#{$name}); } -// given a color name, compute a border color for the color +// Get an explicit hover/background token for a semantic color. @function background-color($name) { - @return color.mix(color.invert(color($name)), color($name), 12.5%); -} - -// escape the color. Note param is a color and not a color name: this is not an accessor to the color map above. -// replaces # with %23 in hex colors -// see https://codepen.io/gunnarbittersmann/pen/BoovjR for explanation of why we have to escape # for the background image -@function escape-color($color) { - $hex: color.ie-hex-str($color); - - @return "%23" + string.slice($string: #{$hex}, $start-at: 4); // skip #AA in #AARRGGBB + @return var(--attack-background-color-#{$name}); } diff --git a/attack-style/abstracts/_variables.scss b/attack-style/abstracts/_variables.scss index bdbc472efa0..9d387c8261b 100644 --- a/attack-style/abstracts/_variables.scss +++ b/attack-style/abstracts/_variables.scss @@ -27,25 +27,25 @@ $user-colors: ( /// $colors: ( primary: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-orange), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-orange); else: map.get($user-colors, user-gray)), on-color: white ), // used for header and some nav elements secondary: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-blue), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-blue); else: map.get($user-colors, user-gray)), on-color: white ), // used for some buttons footer: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-footer), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-footer); else: map.get($user-colors, user-gray)), on-color: #87deff ), // used for footer and some buttons active: ( - color: if(config.$use-attack-theme, map.get($attack-colors, attack-active), map.get($user-colors, user-gray)), + color: if(sass(config.$use-attack-theme): map.get($attack-colors, attack-active); else: map.get($user-colors, user-gray)), on-color: #eaeaea ), // used for active buttons and sidebar links @@ -59,7 +59,7 @@ $colors: ( // body: (color: rgb(50, 50, 50), on-color: #cdcdcd), link: ( - color: #4f7cac + color: #3f709e ), // hyperlinks matrix-header: @@ -75,6 +75,7 @@ $colors: ( on-color: black ), deemphasis: ( - color: #303435 + color: #686f75, + on-color: white ) ); diff --git a/attack-style/components/_matrix.scss b/attack-style/components/_matrix.scss index 0804423c173..a49c57de959 100644 --- a/attack-style/components/_matrix.scss +++ b/attack-style/components/_matrix.scss @@ -123,7 +123,7 @@ $sizeunit: 14px; &.count { font-size: $sizeunit - 1px; - border-bottom: 1px solid black; + border-bottom: 1px solid color-functions.border-color(body); padding-bottom: 5px; margin-bottom: 5px; } @@ -247,7 +247,7 @@ $sizeunit: 14px; &.count { font-size: $sizeunit - 1px; - border-bottom: 1px solid black; + border-bottom: 1px solid color-functions.border-color(body); padding-bottom: 5px; margin-bottom: 5px; } @@ -365,9 +365,9 @@ $sizeunit: 14px; // the menu when the user is clicking. Instead use the // bootstrap hover style. &:active { - color: #16181b; + color: color-functions.on-color(body); text-decoration: none; - background-color: #f8f9fa; + background-color: color-functions.color-alternate(body, 0.8); } } } diff --git a/attack-style/components/_search.scss b/attack-style/components/_search.scss index fbcdaba0216..24b6c9a869c 100644 --- a/attack-style/components/_search.scss +++ b/attack-style/components/_search.scss @@ -232,7 +232,7 @@ padding: 2px 8px; border: 1px solid; border-radius: 4px; - color: white; + color: color-functions.on-color(active); font-size: 0.8rem; font-weight: 700; } @@ -245,6 +245,7 @@ .search-result-badge-domain { border-color: color-functions.color(deemphasis); background: color-functions.color(deemphasis); + color: color-functions.on-color(deemphasis); } .search-no-results .preview { diff --git a/attack-style/layout/_footer.scss b/attack-style/layout/_footer.scss index cd7f7f1cbb4..095dfd31644 100644 --- a/attack-style/layout/_footer.scss +++ b/attack-style/layout/_footer.scss @@ -1,4 +1,3 @@ -@use "sass:color"; @use "../abstracts/color-functions"; @use "../abstracts/utilities"; @@ -53,8 +52,7 @@ color: color-functions.on-color(footer); &:hover { - // add some link color to this so that it resembles a link, but is still visible if normal link color is not visible on footer color - color: color.mix(color-functions.on-color(footer), color-functions.color(link)); + color: color-functions.color(footer-link-hover); } } } diff --git a/attack-style/layout/_layout.scss b/attack-style/layout/_layout.scss index 87fbda5516b..d7bbc97be32 100644 --- a/attack-style/layout/_layout.scss +++ b/attack-style/layout/_layout.scss @@ -1,4 +1,3 @@ -@use "sass:color"; @use "sass:math"; @use "../abstracts/color-functions"; @use "../abstracts/utilities"; @@ -65,6 +64,10 @@ strong { a { color: color-functions.color(link); + &:hover { + color: color-functions.color(link-hover); + } + .anchor::before { content: ""; display: block; @@ -133,6 +136,27 @@ a { tr + tr { border-top: 1px solid color-functions.border-color(body); } + + .external-link-icon { + margin-left: utilities.to-rem(2); + font-size: utilities.to-rem(13); + } + + .random-page-toggle { + margin-left: utilities.to-rem(6); + padding: 0 utilities.to-rem(6); + border: 1px solid color-functions.color(active); + color: color-functions.color(link); + background: color-functions.color(body); + + &:hover, + &:focus, + &[aria-expanded="true"] { + border-color: color-functions.color(secondary); + color: color-functions.on-color(secondary); + background: color-functions.color(secondary); + } + } } .row-main-page { @@ -151,7 +175,7 @@ a { // p for home page .p-line { p { - border-top: 0.0625rem solid #1c2226; + border-top: 0.0625rem solid color-functions.border-color(body); } } @@ -187,7 +211,7 @@ a { @extend .website-button; border-color: color-functions.color(active); - color: #fff; + color: color-functions.on-color(active); background: color-functions.color(active); padding: 6px 16px; @@ -200,7 +224,7 @@ a { @extend .website-button; color: color-functions.color(active); - background: #fff; + background: color-functions.color(body); border-color: color-functions.color(active); padding: 6px 16px; @@ -218,15 +242,21 @@ a { padding-left: 8px; } -.slide-button:hover { +.slide-button:hover, +.slide-button:focus, +.slide-button:active, +.slide-button[aria-expanded="true"] { background: color-functions.color(secondary); border-color: color-functions.color(secondary); + color: color-functions.on-color(secondary); } -.slide-button-secondary:hover { - background: color-functions.on-color(active); +.slide-button-secondary:hover, +.slide-button-secondary:focus, +.slide-button-secondary:active { + background: color-functions.color(secondary); border-color: color-functions.color(secondary); - color: color-functions.color(secondary); + color: color-functions.on-color(secondary); } // used for data sources filter dropdown @@ -238,7 +268,7 @@ a { .dropdown-content { display: none; position: absolute; - background-color: color-functions.on-color(secondary); + background-color: color-functions.color(body); min-width: 160px; box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 20%); } @@ -264,7 +294,7 @@ a { // Extending placeholder 'button-style' is necessary to keep on-color white. Else, the on-color will change to the tag on-color when hovering the button. @extend %button-style; - background-color: color.scale(color-functions.color(secondary), $lightness: 5%); + background-color: color-functions.color(secondary-hover); background-image: none; } } @@ -313,7 +343,7 @@ a { } .active { - color: color-functions.color(primary); + color: color-functions.color(active); } } @@ -336,8 +366,8 @@ a { // table for techniques .table-techniques { thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } table { @@ -347,11 +377,11 @@ a { td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .sub.technique { @@ -369,11 +399,11 @@ a { } .sub.technique td:not(:nth-child(4)) { - color: #4f7cac; + color: color-functions.color(link); } .technique:not(.sub) td:not(:nth-child(3)) { - color: #4f7cac; + color: color-functions.color(link); } } @@ -386,11 +416,11 @@ a { td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .sub.technique { @@ -420,8 +450,8 @@ a { .techniques-used.background { thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } } @@ -436,18 +466,18 @@ a { } thead tr { - background: #f2f2f2; - border-bottom: 2px solid #dee2e6; + background: color-functions.color-alternate(body); + border-bottom: 2px solid color-functions.border-color(body); } td { vertical-align: top; padding: 10px; - border: 1px solid #dfdfdf; + border: 1px solid color-functions.border-color(body); } tr:last-child { - border-bottom: 1px solid #dfdfdf; + border-bottom: 1px solid color-functions.border-color(body); } .datacomponent.datasource { @@ -583,9 +613,18 @@ a { /* BANNER */ .banner-message { - padding: utilities.to-rem(5) 0; + padding: utilities.to-rem(7) utilities.to-rem(16); + border-top: 1px solid color-functions.border-color(banner); + border-bottom: 1px solid color-functions.border-color(banner); text-align: center; - background-color: color-functions.color-alternate(body, 2); + color: color-functions.on-color(banner); + background-color: color-functions.color(banner); + + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + } } // basic banner @@ -622,7 +661,7 @@ pre { } code { - color: #c63e1f; + color: color-functions.color(code); } /* **** */ @@ -657,7 +696,7 @@ code { width: 20%; top: 9.3rem; float: right; - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); } @media screen and (width <= 90.62rem) { @@ -680,7 +719,7 @@ code { .card-header { color: color-functions.on-color(body); - background: rgba(color-functions.on-color(body), 0.03); + background: color-functions.color(card-header); border-bottom-color: color-functions.border-color(body); } @@ -738,7 +777,7 @@ a.partial-underline { } &.background { - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); } } } @@ -795,6 +834,10 @@ a.partial-underline { color: color-functions.on-color-emphasis(body); } +.card-data .card-title { + color: color-functions.color(property-label); +} + .contact-card-title { font-size: 1.1rem; font-weight: bold; @@ -814,7 +857,7 @@ a.partial-underline { max-width: 100%; height: utilities.to-rem(480); margin: 0 auto; - border: 3px solid #dfdfdf; + border: 3px solid color-functions.border-color(body); padding: 3px; display: flex; flex-direction: column; @@ -894,7 +937,7 @@ a.partial-underline { .usa-card__header { @include utilities.font("Roboto-Regular"); - color: color-functions.color(body); + color: color-functions.on-color(secondary); background: color-functions.color(secondary); border-bottom-color: color-functions.border-color(body); border-radius: 0.3rem 0.3rem 0 0; @@ -1024,7 +1067,7 @@ img.yt-core-image { display: inline-block; vertical-align: top; background-position: center; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='#{color-functions.escape-color(color-functions.on-color(body))}' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: var(--attack-select-arrow); z-index: 1; transition: all 0.2s ease; } @@ -1343,7 +1386,7 @@ img.yt-core-image { } .section-shadow { - border-bottom: 1px solid #dfdfdf !important; + border-bottom: 1px solid color-functions.border-color(body) !important; } table { @@ -1372,6 +1415,19 @@ div#sidebars { .attackcons { border-top-width: 0; + .attackcon-banner-image { + display: inline-block; + width: 100%; + box-sizing: border-box; + + &.on-light { + padding: 1rem; + border: 1px solid color-functions.border-color(body); + border-radius: 0.75rem; + background: color-functions.color(image-background); + } + } + .sponsors { flex: 1; padding-left: 25px; @@ -1382,16 +1438,20 @@ div#sidebars { } .sponsors-block { - background: color-functions.on-color(active); + background: color-functions.color(image-background); text-align: center; display: flex; justify-content: space-evenly; flex-wrap: wrap; flex-direction: column; width: 200%; + padding: utilities.to-rem(10); + border: utilities.to-rem(1) solid color-functions.border-color(body); + border-radius: utilities.to-rem(8); + box-sizing: border-box; .img-container { - margin: 10px; + margin: utilities.to-rem(10); flex: 1 1 20%; box-sizing: border-box; @@ -1434,7 +1494,7 @@ div#sidebars { } img.sponsor-logo { - background-color: color-functions.color(body); + background-color: color-functions.color(image-background); border-radius: 6px; object-fit: contain; object-position: center; @@ -1481,7 +1541,7 @@ div#sidebars { .resource { flex: 1; - background-color: color-functions.on-color(active); + background-color: color-functions.color-alternate(body, 2); padding: 10px; box-sizing: border-box; } @@ -1613,7 +1673,7 @@ div#sidebars { } .tip-box { - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); padding: 1rem; } diff --git a/attack-style/layout/_nav.scss b/attack-style/layout/_nav.scss index 85e321a44c3..c25a40d94b4 100644 --- a/attack-style/layout/_nav.scss +++ b/attack-style/layout/_nav.scss @@ -11,6 +11,24 @@ text-decoration: underline; } +@mixin dark-theme-toggle { + .theme-toggle-track { + background: color-functions.color(secondary); + } + + .theme-toggle-icon-light { + color: color-functions.on-color(primary); + } + + .theme-toggle-icon-dark { + color: color-functions.color(secondary); + } + + .theme-toggle-thumb { + transform: translateX(utilities.to-rem(22)); + } +} + /* Top NAVIGATION */ // top navigation bar across the web site .navbar { background-color: color-functions.color(primary); @@ -85,12 +103,100 @@ .search-icon { cursor: pointer; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='#{color-functions.escape-color(color-functions.on-color(primary))}' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); } .error-icon { cursor: default; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='#{color-functions.escape-color(color-functions.on-color(primary))}' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23fff' xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 96 960 960' width='24'%3e%3cpath d='M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z'/%3e%3c/svg%3e"); + } + } + + .theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: utilities.to-rem(58); + min-height: utilities.to-rem(38); + margin-right: utilities.to-rem(8); + padding: utilities.to-rem(4); + border: 0; + color: color-functions.on-color(primary); + + .theme-toggle-track { + position: relative; + display: inline-flex; + align-items: center; + justify-content: space-between; + width: utilities.to-rem(50); + height: utilities.to-rem(28); + padding: 0 utilities.to-rem(7); + border: utilities.to-rem(1) solid color-functions.on-color(primary); + border-radius: utilities.to-rem(14); + background: rgb(0 0 0 / 20%); + box-sizing: border-box; + transition: background-color 0.2s ease; + } + + .theme-toggle-icon { + position: relative; + z-index: 2; + visibility: visible; + display: inline-flex; + align-items: center; + justify-content: center; + width: utilities.to-rem(12); + height: utilities.to-rem(12); + font-size: utilities.to-rem(12); + line-height: 1; + } + + &:hover, + &:focus { + color: color-functions.on-color(primary); + + .theme-toggle-track { + background: rgb(0 0 0 / 35%); + box-shadow: 0 0 0 utilities.to-rem(2) rgb(255 255 255 / 30%); + } + } + + &:focus { + outline: 0; + box-shadow: none; + } + + .theme-toggle-icon-light { + color: color-functions.color(primary); + } + + .theme-toggle-thumb { + position: absolute; + top: utilities.to-rem(2); + left: utilities.to-rem(2); + z-index: 1; + width: utilities.to-rem(22); + height: utilities.to-rem(22); + border-radius: 50%; + background: color-functions.on-color(primary); + box-shadow: 0 utilities.to-rem(1) utilities.to-rem(3) rgb(0 0 0 / 35%); + transition: transform 0.2s ease; + } + + &[data-theme-effective="dark"] { + @include dark-theme-toggle; + } + + // The theme script sets the root preference in the document head. Use it to + // paint the saved state before DOMContentLoaded initializes the control. + :root[data-theme="dark"] & { + @include dark-theme-toggle; + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme]) & { + @include dark-theme-toggle; + } } } } @@ -228,7 +334,7 @@ cursor: col-resize; height: 100%; position: absolute; - background-color: #dfdfdf; + background-color: color-functions.border-color(body); } .data-sources-menu { @@ -286,7 +392,7 @@ .expand-button { // any direct child cursor: pointer; - color: black; + color: color-functions.on-color(body); &:hover { background: color-functions.color-alternate(body); @@ -320,7 +426,7 @@ & > a { color: color-functions.color(active) !important; font-weight: bolder; - background: color-functions.on-color(active); + background: color-functions.color-alternate(body, 2); font-family: Roboto-Bold, sans-serif; } diff --git a/attack-style/package.json b/attack-style/package.json index 490ad27ae26..eaf69c56010 100644 --- a/attack-style/package.json +++ b/attack-style/package.json @@ -8,8 +8,8 @@ "main": "index.js", "scripts": { "clean": "rm -rf dist/", - "build": "sass style-attack.scss dist/style-attack.css && sass style-user.scss dist/style-user.css", - "copy": "cp dist/style-attack.css dist/style-user.css ../attack-theme/static/", + "build": "sass style-attack.scss dist/style-attack.css && sass style-user.scss dist/style-user.css && sass style-archive.scss dist/style-archive.css", + "copy": "mkdir -p ../attack-theme/static && cp dist/style-attack.css dist/style-user.css dist/style-archive.css ../attack-theme/static/", "build-copy": "npm run clean && npm run build && npm run copy", "watch": "sass --watch style-attack.scss dist/style-attack.css && sass --watch style-user.scss dist/style-user.css", "lint": "stylelint **/*.scss" diff --git a/attack-style/style-archive.scss b/attack-style/style-archive.scss new file mode 100644 index 00000000000..3cb39748fbf --- /dev/null +++ b/attack-style/style-archive.scss @@ -0,0 +1,3 @@ +// Compatibility colors for preserved ATT&CK sites; keep their original layout. +@use "config" with ($use-attack-theme: true); +@use "themes/archive"; diff --git a/attack-style/style.scss b/attack-style/style.scss index c8c19234ddd..77fe3a8e71d 100644 --- a/attack-style/style.scss +++ b/attack-style/style.scss @@ -21,4 +21,7 @@ @use "components/search"; // Search component styles @use "components/tour"; // Tour component styles @use "components/matrix"; // Matrix component styles -@use "components/versioning"; // Versioning component styles \ No newline at end of file +@use "components/versioning"; // Versioning component styles + +// Emit runtime light/dark palettes and normalize vendor components last. +@use "themes/colors"; diff --git a/attack-style/themes/README.md b/attack-style/themes/README.md index bca278889a1..e16944148e4 100644 --- a/attack-style/themes/README.md +++ b/attack-style/themes/README.md @@ -1,21 +1,34 @@ # Themes -This folder is reserved for theme-specific Sass. +This folder contains the runtime color palettes used by the generated stylesheets. ## Files | File | Purpose | | --- | --- | -| `_colors.scss` | Reserved for theme color overrides or extracted theme color definitions. It is currently empty. | +| `_palette.scss` | Shared light/dark token mixins, including the charcoal surfaces. | +| `_archive.scss` | Scoped color compatibility rules and a banner switch for preserved sites. | +| `_colors.scss` | Emits the light and dark semantic color tokens, system preference behavior, explicit theme overrides, and shared Bootstrap surface adjustments. | ## Active Theme Switch -The active color set is currently controlled by `config.scss` and the two top-level entrypoints: +The site has two independent theme layers. The brand layer is selected at build time by `config.scss` and the two top-level entrypoints: | File | Behavior | | --- | --- | | `style-attack.scss` | Sets `$use-attack-theme: true` and imports the shared style graph. | | `style-user.scss` | Sets `$use-attack-theme: false` and imports the shared style graph. | -Most theme-aware styling should continue to use semantic color helpers from `abstracts/_color-functions.scss`. -Add theme-specific Sass here only when the existing semantic color map is not enough. +The light/dark appearance is selected at runtime. With no `data-theme` attribute on the root element, the stylesheet follows `prefers-color-scheme`. The single navigation toggle stores an explicit `data-theme="light"` or `data-theme="dark"` override after the user first switches themes. + +On collapsed navigation, the same toggle moves immediately left of the hamburger when it fits alongside the logo. If space is insufficient, it returns to its menu slot before Search; desktop keeps that original slot. Placement follows Bootstrap's hamburger visibility and measured element widths, including changes to the logo or browser size. Saved preferences synchronize across pages and open tabs on the same origin and browser profile. + +Most styling should use semantic color helpers from `abstracts/_color-functions.scss`. Add a token to `_palette.scss` when a component needs a distinct theme-aware surface or foreground instead of embedding a light-only color in that component. + +## Preserved Sites + +`style-archive.scss` builds `style-archive.css` alongside the two current-site stylesheets. The versions deployment module adds this stylesheet and the shared theme script to extracted HTML pages with an archive banner. It skips sidebar fragments, redirects, and pages with native theme controls. The stored tarballs are unchanged; every extraction source receives the same idempotent enhancement. + +The script adds a keyboard-accessible “Dark mode” switch to the existing `.version-banner`. It shares the current site's stored appearance preference. The archive stylesheet supplies color overrides only in dark screen mode; light mode and print retain the archive's original presentation. The archive's original styles, content, URLs, logos, and external widgets remain in place. + +Theme scripts set `data-theme` only. CSS owns `color-scheme`, allowing print to use light native controls even when a dark preference is saved. diff --git a/attack-style/themes/_archive.scss b/attack-style/themes/_archive.scss new file mode 100644 index 00000000000..e0e07aba3f2 --- /dev/null +++ b/attack-style/themes/_archive.scss @@ -0,0 +1,337 @@ +@use "palette"; + +@mixin dark-surfaces { + @include palette.dark-colors; + + color-scheme: dark; + + body, + .jumbotron, + .card, + .card-filter, + .card-body, + .contact-card .card-header.no-background, + .contact-card .card-footer.no-background, + .sidebar, + .sidebar .heading, + .matrix-container, + .matrix .technique-cell, + .table-matrix td, + .dropdown-content, + .dropdown-menu, + .form-control, + .custom-select, + .bootstrap-select > .dropdown-toggle, + .modal-content, + .popover, + .popover-body, + .search-results, + .overlay.search .overlay-inner, + .overlay.search .overlay-inner .search-header .search-input input { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + + .table, + .table-light, + .table td, + .table th, + .card-data, + .getting-started-color, + .table-techniques td, + .techniques-used td, + .datasources-table td { + color: var(--attack-on-color-body); + border-color: var(--attack-border-color-body); + } + + a:where(:not(.osano-cm-link)), + .dropdown-item, + .matrix-tactics-url, + .matrix-tactics-url:visited, + .matrix-tactics-url:hover, + .matrix-tactics-url:active, + .table-techniques .sub.technique td:not(:nth-child(4)), + .table-techniques .technique:not(.sub) td:not(:nth-child(3)) { + color: var(--attack-color-link); + } + + .matrix-header { + color: var(--attack-on-color-body-emphasis); + } + + .table-light, + .table-matrix .matrix-header, + .table-alternate tbody, + .blog-post table tbody, + .changelog table tbody { + background-color: var(--attack-color-body); + } + + .bg-white { + background-color: var(--attack-color-body) !important; + } + + .bg-alternate, + .bg-gray, + .bg-light, + .bg-accord-light, + .bg-accord-dark, + .table-alternate, + .table-techniques thead tr, + .techniques-used thead tr, + .datasources-table thead tr, + .table-striped tbody tr:nth-of-type(odd), + .card-header, + .contact-card .card-body.background, + .breadcrumb, + .resource, + .tip-box, + .under-development, + .training .exercise, + .example-container, + .section-view .anchor-section { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate) !important; + border-color: var(--attack-border-color-body); + } + + .table .active, + .search-results .search-header, + .search-results .search-highlight, + .nav-link.side.active { + color: var(--attack-color-active); + } + + .nav .heading-dropdown, + .faq .heading-dropdown, + .faq .nav-link.expand-title, + .nav .nav-link.expand-title, + .expand-icon, + .card-title { + color: var(--attack-on-color-body-emphasis); + } + + .version-banner, + .version-banner a { + color: var(--attack-on-color-banner); + background-color: var(--attack-color-banner); + } + + .version-banner a { + text-decoration: underline; + } + + .footer a, + .footer .footer-link { + color: var(--attack-on-color-footer); + } + + .table-hover tbody tr:hover, + .dropdown-item:hover, + .dropdown-item:focus, + .nav-link.side:hover, + .sidenav .sidenav-head a:hover, + .sidenav .sidenav-head .expand-button:hover { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate-strong); + } + + .sidenav .sidenav-head.active, + .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button { + color: var(--attack-on-color-body-emphasis); + } + + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head a:hover, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button:hover { + background-color: var(--attack-color-body-alternate-strong); + } + + .nav .nav-link.side.active, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head.active > a { + color: var(--attack-color-active) !important; + background-color: var(--attack-color-body-alternate-strong); + } + + .card-data .card-title { + color: var(--attack-color-property-label); + } + + .deemphasis, + .text-label-small, + .text-muted { + color: var(--attack-on-color-body-deemphasis) !important; + } + + .text-danger, + font[color="red"] { + color: var(--attack-color-danger) !important; + } + + .search-word-found, + mark { + color: var(--attack-on-color-search-highlight); + background-color: var(--attack-color-search-highlight); + } + + .custom-select, + .card-block .card-header::after, + .faq .card-header::after, + .heading-dropdown::after, + .sidebar.nav .sidenav-wrapper .sidenav-list .sidenav .sidenav-head .expand-button::after, + #usecases .card-header::after { + background-image: var(--attack-select-arrow); + } + + .resizer { + background-color: var(--attack-border-color-body); + } + + code { + color: var(--attack-color-code); + } + + pre, + .jumbotron code { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body-alternate-strongest); + } + + .btn-default, + .btn-outline-secondary, + .matrix-controls button, + .matrix-controls .layout-button:active, + .slide-button-secondary { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); + } + + .btn-primary, + .footer .btn-primary { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + + .btn-default:hover, + .btn-outline-secondary:not(:disabled, .disabled):hover, + .btn-outline-secondary:not(:disabled, .disabled):focus, + .matrix-controls button:hover, + .slide-button { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } + + .matrix.side .tactic .handle, + .matrix.flat .tactic .supertechnique td.sidebar.technique .handle { + color: var(--attack-color-body); + background-color: var(--attack-on-color-body-deemphasis); + } + + .matrix.side .sidebar.expanded .angle, + .matrix.side .tactic .sidebar.expanded .angle { + background-color: var(--attack-color-body); + } + + .matrix.side .tactic:hover:not(.name, .count), + .matrix.side .tactic:hover:not(.name, .count) .sidebar.expanded .angle { + background-color: var(--attack-background-color-body); + } + + .matrix-container .scroll-indicator-group .scroll-indicator.right.show .cover { + background: linear-gradient(to right, rgb(255 255 255 / 0.1%), var(--attack-color-body)); + } + + .matrix-container .scroll-indicator-group .scroll-indicator.left.show .cover { + background: linear-gradient(to left, rgb(255 255 255 / 0.1%), var(--attack-color-body)); + } + + .matrix .tactic.count, + .matrix .technique-cell, + .resizer, + hr { + border-color: var(--attack-border-color-body); + } + + // Header/footer retain their original branding. Do not recolor images or + // third-party widgets; their own palettes and functionality belong to them. + .navbar-orange .nav-link, + .navbar .nav-tabs .nav-link, + .nav .dropdown-menu .dropdown-item { + color: white; + } + + .nav .dropdown-menu { + background-color: var(--attack-color-primary); + } + +} + +:root[data-archive-theme] { + @include palette.light-colors; + + color-scheme: light; + + .archive-theme-toggle { + display: inline-flex; + align-items: center; + gap: 0.5em; + margin: 0.25em 0.75em; + padding: 0.35em 0.65em; + min-height: 2.75em; + border: 1px solid currentcolor; + border-radius: 0.3em; + color: inherit; + background: transparent; + font: inherit; + cursor: pointer; + + &::after { + content: ""; + width: 2em; + height: 1em; + border: 1px solid currentcolor; + border-radius: 1em; + background: radial-gradient(circle at 0.5em center, currentcolor 0.3em, transparent 0.35em); + } + + &[aria-checked="true"]::after { + background: radial-gradient(circle at 1.5em center, currentcolor 0.3em, transparent 0.35em); + } + + &:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 3px; + } + } +} + +// Restrict all compatibility overrides to the dark screen appearance. Light +// and print retain the archive's original CSS, including native controls. +@media screen { + :root[data-archive-theme][data-theme="dark"] { + @include dark-surfaces; + } +} + +@media screen and (prefers-color-scheme: dark) { + :root[data-archive-theme]:not([data-theme]) { + @include dark-surfaces; + } +} + +@media print { + :root[data-archive-theme] .archive-theme-toggle { + display: none; + } +} diff --git a/attack-style/themes/_colors.scss b/attack-style/themes/_colors.scss index e69de29bb2d..5a96b3ceaea 100644 --- a/attack-style/themes/_colors.scss +++ b/attack-style/themes/_colors.scss @@ -0,0 +1,130 @@ +@use "palette"; + +:root, +:root[data-theme="light"] { + @include palette.light-colors; + + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + @include palette.dark-colors; + + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + @include palette.dark-colors; + + color-scheme: dark; +} + +// Normalize Bootstrap surfaces that otherwise retain hard-coded light colors. +.form-control, +.custom-select, +.bootstrap-select > .dropdown-toggle, +.dropdown-menu, +.list-group-item, +.modal-content, +.popover, +.popover-body, +.page-link, +.input-group-text { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body); +} + +.custom-select { + background-image: var(--attack-select-arrow); +} + +.form-control:focus, +.custom-select:focus { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-color-active); + box-shadow: 0 0 0 0.2rem rgb(76 159 254 / 25%); +} + +.dropdown-item, +.page-link { + color: var(--attack-color-link); +} + +.dropdown-item:hover, +.dropdown-item:focus, +.page-link:hover, +.page-link:focus { + color: var(--attack-on-color-body-emphasis); + background-color: var(--attack-color-body-alternate); +} + +.form-control:disabled, +.form-control[readonly], +.custom-select:disabled, +.page-item.disabled .page-link { + color: var(--attack-on-color-body-deemphasis); + background-color: var(--attack-color-body-alternate); +} + +.dropdown-divider, +hr { + border-color: var(--attack-border-color-body); +} + +.table { + color: var(--attack-on-color-body); +} + +.text-danger { + color: var(--attack-color-danger) !important; +} + +.btn-outline-secondary { + color: var(--attack-on-color-body); + border-color: var(--attack-on-color-body-deemphasis); + + &:disabled, + &.disabled { + color: var(--attack-on-color-body-deemphasis); + background-color: transparent; + } + + &:not(:disabled, .disabled):hover, + &:not(:disabled, .disabled):focus, + &:not(:disabled, .disabled):active { + color: var(--attack-on-color-active); + background-color: var(--attack-color-active); + border-color: var(--attack-color-active); + } +} + +.nav-tabs .nav-link { + color: var(--attack-color-link); + border-color: transparent; +} + +.nav-tabs .nav-link:hover, +.nav-tabs .nav-link:focus { + border-color: var(--attack-border-color-body); +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--attack-on-color-body); + background-color: var(--attack-color-body); + border-color: var(--attack-border-color-body) var(--attack-border-color-body) var(--attack-color-body); +} + +@media print { + :root, + :root[data-theme], + :root:not([data-theme]) { + @include palette.light-colors; + + color-scheme: light; + } +} diff --git a/attack-style/themes/_palette.scss b/attack-style/themes/_palette.scss new file mode 100644 index 00000000000..d074e1ce87f --- /dev/null +++ b/attack-style/themes/_palette.scss @@ -0,0 +1,92 @@ +@use "sass:color"; +@use "sass:map"; +@use "../abstracts/variables"; +@use "../config" as config; + +$primary: map.get(map.get(variables.$colors, primary), color); +$secondary: map.get(map.get(variables.$colors, secondary), color); +$footer: map.get(map.get(variables.$colors, footer), color); +$active: map.get(map.get(variables.$colors, active), color); +$dark-active: if(sass(config.$use-attack-theme): #60a9ff; else: #9aa3a6); + +@mixin light-colors { + --attack-color-primary: #{$primary}; + --attack-on-color-primary: white; + --attack-color-secondary: #{$secondary}; + --attack-color-secondary-hover: #{color.scale($secondary, $lightness: 5%)}; + --attack-on-color-secondary: white; + --attack-color-footer: #{$footer}; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #{color.mix(#87deff, #3f709e)}; + --attack-color-active: #{$active}; + --attack-color-active-alternate-medium: #{color.mix(color.invert($active), $active, $weight: 7.5%)}; + --attack-on-color-active: #eaeaea; + --attack-color-body: white; + --attack-on-color-body: #39434c; + --attack-on-color-body-emphasis: #1d2226; + --attack-color-property-label: #1d2226; + --attack-on-color-body-deemphasis: #6b7379; + --attack-color-body-alternate-subtle: #f5f5f5; + --attack-color-body-alternate: #f2f2f2; + --attack-color-body-alternate-strong: #e6e6e6; + --attack-color-body-alternate-strongest: #d9d9d9; + --attack-border-color-body: #dfdfdf; + --attack-background-color-body: #dfdfdf; + --attack-color-link: #3f709e; + --attack-color-link-hover: #0056b3; + --attack-color-matrix-header: gray; + --attack-on-color-matrix-header: white; + --attack-color-search-highlight: yellow; + --attack-on-color-search-highlight: black; + --attack-color-deemphasis: #686f75; + --attack-on-color-deemphasis: white; + --attack-color-card-header: rgb(57 67 76 / 3%); + --attack-color-code: #a52f16; + --attack-color-danger: #bd2130; + --attack-color-image-background: white; + --attack-color-banner: #e7f0f6; + --attack-on-color-banner: #263b4a; + --attack-border-color-banner: #c2d5e2; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%2339434c' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); +} + +@mixin dark-colors { + --attack-color-primary: #{$primary}; + --attack-on-color-primary: white; + --attack-color-secondary: #{$secondary}; + --attack-color-secondary-hover: #{color.scale($secondary, $lightness: 8%)}; + --attack-on-color-secondary: white; + --attack-color-footer: #{$footer}; + --attack-on-color-footer: #87deff; + --attack-color-footer-link-hover: #b7edff; + --attack-color-active: #{$dark-active}; + --attack-color-active-alternate-medium: #{if(sass(config.$use-attack-theme): #3d8cdb; else: #879195)}; + --attack-on-color-active: #0f171c; + --attack-color-body: #222426; + --attack-on-color-body: #e8e6e3; + --attack-on-color-body-emphasis: #fffaf4; + --attack-color-property-label: #f2d2a4; + --attack-on-color-body-deemphasis: #b7b1a8; + --attack-color-body-alternate-subtle: #272a2c; + --attack-color-body-alternate: #2b2e30; + --attack-color-body-alternate-strong: #303437; + --attack-color-body-alternate-strongest: #353a3d; + --attack-border-color-body: #596166; + --attack-background-color-body: #373d40; + --attack-color-link: #7bb8ee; + --attack-color-link-hover: #b7ddff; + --attack-color-matrix-header: #596166; + --attack-on-color-matrix-header: #fffaf4; + --attack-color-search-highlight: #665a00; + --attack-on-color-search-highlight: #fff4b8; + --attack-color-deemphasis: #b7b1a8; + --attack-on-color-deemphasis: #222426; + --attack-color-card-header: #2b2e30; + --attack-color-code: #ff8f70; + --attack-color-danger: #ff8c96; + --attack-color-image-background: white; + --attack-color-banner: #263a49; + --attack-on-color-banner: #f2f7fa; + --attack-border-color-banner: #3f5d72; + --attack-select-arrow: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23e8e6e3' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z'/%3E%3C/svg%3E"); +} diff --git a/attack-theme/static/scripts/search_bundle.js b/attack-theme/static/scripts/search_bundle.js new file mode 100644 index 00000000000..c5445edad75 --- /dev/null +++ b/attack-theme/static/scripts/search_bundle.js @@ -0,0 +1 @@ +(()=>{var e={376(e,t,n){function r(e,t,n,r,i,o,u){try{var a=e[o](u),s=a.value}catch(e){return void n(e)}a.done?t(s):Promise.resolve(s).then(r,i)}function i(e){return function(){var t=this,n=arguments;return new Promise((function(i,o){var u=e.apply(t,n);function a(e){r(u,i,o,a,s,"next",e)}function s(e){r(u,i,o,a,s,"throw",e)}a(void 0)}))}}function o(){var e,t,n="function"==typeof Symbol?Symbol:{},r=n.iterator||"@@iterator",i=n.toStringTag||"@@toStringTag";function a(n,r,i,o){var a=r&&r.prototype instanceof c?r:c,l=Object.create(a.prototype);return u(l,"_invoke",function(n,r,i){var o,u,a,c=0,l=i||[],f=!1,h={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,n){return o=t,u=0,a=e,h.n=n,s}};function d(n,r){for(u=n,a=r,t=0;!f&&c&&!i&&t3?(i=p===r)&&(a=o[(u=o[4])?5:(u=3,3)],o[4]=o[5]=e):o[0]<=d&&((i=n<2&&dr||r>p)&&(o[4]=n,o[5]=r,h.n=p,u=0))}if(i||n>1)return s;throw f=!0,r}return function(i,l,p){if(c>1)throw TypeError("Generator is already running");for(f&&1===l&&d(l,p),u=l,a=p;(t=u<2?e:a)||!f;){o||(u?u<3?(u>1&&(h.n=-1),d(u,a)):h.n=a:h.v=a);try{if(c=2,o){if(u||(i="next"),t=o[i]){if(!(t=t.call(o,a)))throw TypeError("iterator result is not an object");if(!t.done)return t;a=t.value,u<2&&(u=0)}else 1===u&&(t=o.return)&&t.call(o),u<2&&(a=TypeError("The iterator does not provide a '"+i+"' method"),u=1);o=e}else if((t=(f=h.n<0)?a:n.call(r,h))!==s)break}catch(t){o=e,u=1,a=t}finally{c=1}}return{value:t,done:f}}}(n,i,o),!0),l}var s={};function c(){}function l(){}function f(){}t=Object.getPrototypeOf;var h=[][r]?t(t([][r]())):(u(t={},r,(function(){return this})),t),d=f.prototype=c.prototype=Object.create(h);function p(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,f):(e.__proto__=f,u(e,i,"GeneratorFunction")),e.prototype=Object.create(d),e}return l.prototype=f,u(d,"constructor",f),u(f,"constructor",l),l.displayName="GeneratorFunction",u(f,i,"GeneratorFunction"),u(d),u(d,i,"Generator"),u(d,r,(function(){return this})),u(d,"toString",(function(){return"[object Generator]"})),(o=function(){return{w:a,m:p}})()}function u(e,t,n,r){var i=Object.defineProperty;try{i({},"",{})}catch(e){i=0}u=function(e,t,n,r){function o(t,n){u(e,t,(function(e){return this._invoke(t,n,e)}))}t?i?i(e,t,{value:n,enumerable:!r,configurable:!r,writable:!r}):e[t]=n:(o("next",0),o("throw",1),o("return",2))},u(e,t,n,r)}function a(e,t,n){if(c())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,t);var i=new(e.bind.apply(e,r));return n&&s(i,n.prototype),i}function s(e,t){return s=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},s(e,t)}function c(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(c=function(){return!!e})()}function l(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return f(e,t);var n={}.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?f(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,u=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return u=e.done,e},e:function(e){a=!0,o=e},f:function(){try{u||null==n.return||n.return()}finally{if(a)throw o}}}}function f(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n0&&void 0!==arguments[0]?arguments[0]:{};if(!this||this.constructor!==D)return a(D,Array.prototype.slice.call(arguments));if(arguments.length)for(e=0;e1?this.addMatcher(e,t):(this.mapper||(this.mapper=new Map),this.mapper.set(e,t),this.cache&&A(this),this)},u.addMatcher=function(e,t){return"object"===h(e)?this.addReplacer(e,t):e.length<2&&(this.dedupe||this.mapper)?this.addMapper(e,t):(this.matcher||(this.matcher=new Map),this.matcher.set(e,t),this.h+=(this.h?"|":"")+e,this.J=null,this.cache&&A(this),this)},u.addReplacer=function(e,t){return"string"==typeof e?this.addMatcher(e,t):(this.replacer||(this.replacer=[]),this.replacer.push(e,t),this.cache&&A(this),this)},u.encode=function(e,t){var n=this;if(this.cache&&e.length<=this.H)if(this.F){if(this.B.has(e))return this.B.get(e)}else this.F=setTimeout(A,50,this);this.normalize&&(e="function"==typeof this.normalize?this.normalize(e):w?e.normalize("NFKD").replace(w,"").toLowerCase():e.toLowerCase()),this.prepare&&(e=this.prepare(e)),this.numeric&&e.length>3&&(e=e.replace(g,"$1 $2").replace(m,"$1 $2").replace(b,"$1 "));for(var r,i,o,u,a=!(this.dedupe||this.mapper||this.filter||this.matcher||this.stemmer||this.replacer),s=[],c=f(),l=this.split||""===this.split?e.split(this.split):[e],h=0;hthis.maxlength)){if(t){if(c[o])continue;c[o]=1}else{if(r===o)continue;r=o}if(a)s.push(o);else if(!this.filter||("function"==typeof this.filter?this.filter(o):!this.filter.has(o))){if(this.cache&&o.length<=this.I)if(this.F){var d=this.D.get(o);if(d||""===d){d&&s.push(d);continue}}else this.F=setTimeout(A,50,this);if(this.stemmer){this.K||(this.K=new RegExp("(?!^)("+this.A+")$"));for(var p=void 0;p!==o&&o.length>2;)p=o,o=o.replace(this.K,(function(e){return n.stemmer.get(e)}))}if(o&&(this.mapper||this.dedupe&&o.length>1)){d="";for(var y,v,D=0,E="";D1&&(this.J||(this.J=new RegExp("("+this.h+")","g")),o=o.replace(this.J,(function(e){return n.matcher.get(e)}))),o&&this.replacer)for(d=0;o&&dthis.L&&(this.D.clear(),this.I=this.I/1.1|0)),o){if(o!==u)if(t){if(c[o])continue;c[o]=1}else{if(i===o)continue;i=o}s.push(o)}}}return this.finalize&&(s=this.finalize(s)||s),this.cache&&e.length<=this.H&&(this.B.set(e,s),this.B.size>this.L&&(this.B.clear(),this.H=this.H/1.1|0)),s},F.prototype.set=function(e,t){this.cache.set(this.h=e,t),this.cache.size>this.limit&&this.cache.delete(this.cache.keys().next().value)},F.prototype.get=function(e){var t=this.cache.get(e);return t&&this.h!==e&&(this.cache.delete(e),this.cache.set(this.h=e,t)),t},F.prototype.remove=function(e){var t,n=l(this.cache);try{for(n.s();!(t=n.n()).done;){var r=t.value,i=r[0];r[1].includes(e)&&this.cache.delete(i)}}catch(e){n.e(e)}finally{n.f()}},F.prototype.clear=function(){this.cache.clear(),this.h=""};var _,x,S,C,j,O={normalize:!1,numeric:!1,dedupe:!1},k={},B=new Map([["b","p"],["v","f"],["w","f"],["z","s"],["x","s"],["d","t"],["n","m"],["c","k"],["g","k"],["j","k"],["q","k"],["i","e"],["y","e"],["u","o"]]),T=new Map([["ae","a"],["oe","o"],["sh","s"],["kh","k"],["th","t"],["ph","f"],["pf","f"]]),P=[/([^aeo])h(.)/g,"$1$2",/([aeo])h([^aeo]|$)/g,"$1$2",/(.)\1+/g,"$1"],I={a:"",e:"",i:"",o:"",u:"",y:"",b:1,f:1,p:1,v:1,c:2,g:2,j:2,k:2,q:2,s:2,x:2,z:2,ß:2,d:3,t:3,l:4,m:5,n:5,r:6},N={Exact:O,Default:k,Normalize:k,LatinBalance:{mapper:B},LatinAdvanced:{mapper:B,matcher:T,replacer:P},LatinExtra:{mapper:B,replacer:P.concat([/(?!^)[aeo]/g,""]),matcher:T},LatinSoundex:{dedupe:!1,include:{letter:!0},finalize:function(e){for(var t=0;t=(i=e.index[u]).length)t-=i.length;else{var a=(t=i[r?"splice":"slice"](t,n)).length;if(a&&(o=o.length?o.concat(t):t,n-=a,r&&(e.length-=a),!n))break;t=0}return o}function M(e){if(!this||this.constructor!==M)return new M(e);this.index=e?[e]:[],this.length=e?e.length:0;var t=this;return new Proxy([],{get:function(e,n){return"length"===n?t.length:"push"===n?function(e){t.index[t.index.length-1].push(e),t.length++}:"pop"===n?function(){if(t.length)return t.length--,t.index[t.index.length-1].pop()}:"indexOf"===n?function(e){for(var n,r,i=0,o=0;o=0)return i+r;i+=n.length}return-1}:"includes"===n?function(e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:8;if(!this||this.constructor!==q)return new q(e);this.index=f(),this.h=[],this.size=0,e>32?(this.B=$,this.A=BigInt(e)):(this.B=K,this.A=e)}function L(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:8;if(!this||this.constructor!==L)return new L(e);this.index=f(),this.h=[],this.size=0,e>32?(this.B=$,this.A=BigInt(e)):(this.B=K,this.A=e)}function K(e){var t=Math.pow(2,this.A)-1;if("number"==typeof e)return e&t;for(var n=0,r=this.A+1,i=0;i=this.priority*this.priority*3):(S=setTimeout(W,0),C=Date.now()),j){var i=this;return new Promise((function(t){setTimeout((function(){t(i[e+"Async"].apply(i,n))}),0)}))}var o=this[e].apply(this,n);return r=o.then?o:new Promise((function(e){return e(o)})),t&&r.then(t),r}}M.prototype.clear=function(){this.index.length=0},M.prototype.push=function(){},q.prototype.get=function(e){var t=this.index[this.B(e)];return t&&t.get(e)},q.prototype.set=function(e,t){var n=this.B(e),r=this.index[n];r?(n=r.size,r.set(e,t),(n-=r.size)&&this.size++):(this.index[n]=r=new Map([[e,t]]),this.h.push(r),this.size++)},L.prototype.add=function(e){var t=this.B(e),n=this.index[t];n?(t=n.size,n.add(e),(t-=n.size)&&this.size++):(this.index[t]=n=new Set([e]),this.h.push(n),this.size++)},(u=q.prototype).has=L.prototype.has=function(e){var t=this.index[this.B(e)];return t&&t.has(e)},u.delete=L.prototype.delete=function(e){var t=this.index[this.B(e)];t&&t.delete(e)&&this.size--},u.clear=L.prototype.clear=function(){this.index=f(),this.h=[],this.size=0},u.values=L.prototype.values=o().m((function e(){var t,n,r,i,u;return o().w((function(e){for(;;)switch(e.p=e.n){case 0:t=0;case 1:if(!(t0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;function i(n){function r(e){var t=(e=e.data||e).id,n=t&&a.h[t];n&&(n(e.msg),delete a.h[t])}if(this.worker=n,this.h=f(),this.worker)return u?this.worker.on("message",r):this.worker.onmessage=r,e.config?new Promise((function(t){G>1e9&&(G=0),a.h[++G]=function(){t(a)},a.worker.postMessage({id:G,task:"init",factory:o,options:e})})):(this.priority=e.priority||4,this.encoder=t||null,this.worker.postMessage({task:"init",factory:o,options:e}),this)}if(!this||this.constructor!==Y)return new Y(e);var o=void 0!==r?r._factory:"undefined"!=typeof window?window._factory:null;o&&(o=o.toString());var u="undefined"==typeof window,a=this,s=function(e,t,r){return t?new(n(809).Worker)("//node/node.js"):e?new window.Worker(URL.createObjectURL(new Blob(["onmessage="+U.toString()],{type:"text/javascript"}))):new window.Worker("string"==typeof r?r:(0,eval)("import.meta.url").replace("/worker.js","/worker/worker.js").replace("flexsearch.bundle.module.min.js","module/worker/worker.js").replace("flexsearch.bundle.module.min.mjs","module/worker/worker.js"),{type:"module"})}(o,u,e.worker);return s.then?s.then((function(e){return i.call(a,e)})):i.call(this,s)}function X(e){Y.prototype[e]=function(){var t,n=this,r=[].slice.call(arguments),i=r[r.length-1];return"function"==typeof i&&(t=i,r.pop()),i=new Promise((function(t){"export"===e&&"function"==typeof r[0]&&(r[0]=null),G>1e9&&(G=0),n.h[++G]=t,n.worker.postMessage({task:e,id:G,args:r})})),t?(i.then(t),this):i}}function J(e,t,n,r,i,o){if(e=e[i],r===n.length-1)t[i]=o||e;else if(e)if(e.constructor===Array)for(t=t[i]=Array(e.length),i=0;it?e.slice(n,n+t):e,r?pe.call(this,e):e;for(var i,o,u=[],a=0;a=o){n-=o;continue}o=(i=i.slice(n,n+t)).length,n=0}if(o>t&&(i=i.slice(0,t),o=t),!u.length&&o>=t)return r?pe.call(this,i):i;if(u.push(i),!(t-=o))break}return u=u.length>1?[].concat.apply([],u):u[0],r?pe.call(this,u):u}function ee(e,t,n,r){var i=r[0];if(i[0]&&i[0].query)return e[t].apply(e,i);if(!("and"!==t&&"not"!==t||e.result.length||e.await||i.suggest))return r.length>1&&(i=r[r.length-1]),(r=i.resolve)?e.await||e.result:e;var o,u,a,s,c,l,f=[],h=0,d=0,p=function(){if(i=r[t]){if(l=void 0,i.constructor===ae)l=i.await||i.result;else if(i.then||i.constructor===Array)l=i;else{h=i.limit||0,d=i.offset||0,a=i.suggest,u=i.resolve,o=((s=i.highlight||e.highlight)||i.enrich)&&u,l=i.queue;var n=i.async||l,p=i.index,y=i.query;if(p?e.index||(e.index=p):p=e.index,y||i.tag){var v=i.field||i.pluck;if(v&&(!y||e.query&&!s||(e.query=y,e.field=v,e.highlight=s),p=p.index.get(v)),l&&(c||e.await)){var b;c=1;var g=e.C.length,m=new Promise((function(e){b=e}));return w=p,D=Object.assign({},i),m.h=function(){D.index=null,D.resolve=!1;var t=n?w.searchAsync(D):w.search(D);return t.then?t.then((function(t){return e.C[g]=t=t.result||t,b(t),t})):(t=t.result||t,b(t),t)},e.C.push(m),f[t]=m,0}i.resolve=!1,i.index=null,l=n?p.searchAsync(i):p.search(i),i.resolve=u,i.index=p}else if(i.and)l=te(i,"and",p);else if(i.or)l=te(i,"or",p);else if(i.not)l=te(i,"not",p);else{if(!i.xor)return 0;l=te(i,"xor",p)}}l.await?(c=1,l=l.await):l.then?(c=1,l=l.then((function(e){return e.result||e}))):l=l.result||l,f[t]=l}var w,D};for(t=0;t1&&(n=n[t].apply(n,e.slice(1))),n}function ne(e,t,n,r,i,o,u){return e.length&&(this.result.length&&e.push(this.result),e.length<2?this.result=e[0]:(this.result=le(e,t,n,!1,this.h),n=0)),i&&(this.await=null),i?this.resolve(t,n,r,u):this}function re(e,t,n,r,i,o,u){if(!o&&!this.result.length)return i?this.result:this;var a;if(e.length)if(this.result.length&&e.unshift(this.result),e.length<2)this.result=e[0];else{for(var s,c,l=0,f=0;f1?B.join(" "):B[0])&&k){for(var P=k.length,I=(b.split?k.replace(b.split,""):k).length-B.length,N="",R=0,M=0;M-1&&(N=(q?k.substring(0,q):"")+u+k.substring(q,q+L)+a+(q+L=l)break}else E+=(E?" ":"")+(k=F[O]),l&&_.push({text:k})}if(j=x.length*(o.length-2),s||c||l&&E.length-j>l)if(O=C-S,s>0&&(O+=s),c>0&&(O+=c),O<=(j=l+j-2*v))F=s?S-(s>0?s:0):S-((j-O)/2|0),_=c?C+(c>0?c:0):F+j,f||(F>0&&" "!==E.charAt(F)&&" "!==E.charAt(F-1)&&((F=E.indexOf(" ",F))<0&&(F=0)),_=_.length-1){if(K>=_.length){O[U+1]=1,K>=F.length&&(j[U+1]=1);continue}P-=v}if(E=_[K].text,L=c&&B[U]){if(!(L>0)){O[U+1]=1;continue}if(E.length>L){if(O[U+1]=1,!f)continue;E=E.substring(0,L)}(L-=E.length)||(L=-1),B[U]=L}if(P+E.length+1<=l)E=" "+E,C[U]+=E;else{if(!f){O[U+1]=1;continue}($=l-P-1)>0&&(E=" "+E.substring(0,$),C[U]+=E),O[U+1]=1}}else{if(O[U])continue;if(S[K-=I]){P-=v,O[U]=1,j[U]=1;continue}if(K<=0){if(K<0){O[U]=1,j[U]=1;continue}P-=v}if(E=_[K].text,L=s&&k[U]){if(!(L>0)){O[U]=1;continue}if(E.length>L){if(O[U]=1,!f)continue;E=E.substring(E.length-L)}(L-=E.length)||(L=-1),k[U]=L}if(P+E.length+1<=l)E+=" ",C[U]=E+C[U];else{if(!f){O[U]=1;continue}($=E.length+1-(l-P))>=0&&$=F.length-1||K<_.length-1&&_[K+1].match?V=1:v&&(P+=v),P-=o.length-2,U&&!(P+E.length<=l)){$=R=M=j[U]=0;break}C[U]=E,V&&(j[U+1]=1,O[U+1]=1)}P+=E.length,$=S[K]=1}if($)I===N?N++:I++;else{if(I===N?R=0:M=0,!R&&!M)break;R?N=++I:N++}}E="";for(var H=0;H1?le(l,n,r,u,o):(l=l[0])&&n&&l.length>n||r?l.slice(r,n+r):l;else{if(en||r)&&(l=l.slice(r,n+r));else{i=[];for(var w,D=0;Dr)r-=w.length;else if((n&&w.length>n||r)&&(n-=(w=w.slice(r,n+r)).length,r&&(r-=w.length)),i.push(w),!n)break;l=i}}return l}function le(e,t,n,r,i){var o,u,a=[],s=f(),c=e.length;if(r){for(i=c-1;i>=0;i--)if(u=(r=e[i])&&r.length)for(c=0;c=0;h--){l=e[h];for(var p=0;p0&&((n&&t>n||r)&&(e=e.slice(r,r+n)),i&&(e=pe.call(this,e))),e):[]}function pe(e){if(!this||!this.store)return e;if(this.db)return this.index.get(this.field[0]).db.enrich(e);for(var t,n=Array(e.length),r=0;r1?n:n[0]}function ge(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=[],r=[];t&&(t=25e4/t*5e3|0);var i,o=l(e.entries());try{for(o.s();!(i=o.n()).done;){var u=i.value;r.push(u),r.length===t&&(n.push(r),r=[])}}catch(e){o.e(e)}finally{o.f()}return r.length&&n.push(r),n}function me(e,t){t||(t=new Map);for(var n,r=0;r1&&void 0!==arguments[1]?arguments[1]:0,n=[],r=[];t&&(t=25e4/t*1e3|0);var i,o=l(e.entries());try{for(o.s();!(i=o.n()).done;){var u=i.value;r.push([u[0],ge(u[1])[0]||[]]),r.length===t&&(n.push(r),r=[])}}catch(e){o.e(e)}finally{o.f()}return r.length&&n.push(r),n}function De(e,t){t||(t=new Map);for(var n,r,i=0;i6&&void 0!==arguments[6]?arguments[6]:0,a=r&&r.constructor===Array,s=a?r.shift():r;if(!s)return this.export(e,t,i,o+1);if((s=e((t?t+".":"")+(u+1)+"."+n,JSON.stringify(s)))&&s.then){var c=this;return s.then((function(){return Fe.call(c,e,t,n,a?r:null,i,o,u+1)}))}return Fe.call(this,e,t,n,a?r:null,i,o,u+1)}function _e(e,t){var n,r="",i=l(e.entries());try{for(i.s();!(n=i.n()).done;){var o=n.value;e=o[0];for(var u,a=o[1],s="",c=0;c=0){if(i.length>1)return i.splice(o,1),1;if(delete e[a],n)return 1;u=1}else{if(u)return 1;n++}}}else{var s,c=l(e.entries());try{for(c.s();!(s=c.n()).done;){var f=s.value;r=f[0],xe(f[1],t)?n++:e.delete(r)}}catch(e){c.e(e)}finally{c.f()}}return n}X("add"),X("append"),X("search"),X("update"),X("remove"),X("clear"),X("export"),X("import"),Y.prototype.searchCache=E,H(Y.prototype),ye.prototype.add=function(e,t,n){if(p(e)&&(e=y(t=e,this.key)),t&&(e||0===e)){if(!n&&this.reg.has(e))return this.update(e,t);for(var r,i=0;i1?ce(y,1,0,0,u,a):y[0],O)}))}return g?y:new ae(y.length>1?ce(y,1,0,0,u,a):y[0],this)}}g||i||!(l=l||this.field)||(d(l)?i=l:(l.constructor===Array&&1===l.length&&(l=l[0]),i=l.field||l.index)),l&&l.constructor!==Array&&(l=[l])}l||(l=this.field),E=(this.worker||this.db)&&!r&&[];for(var k,B,T,P=0;P-1&&(f.length>1?f.splice(h,1):a.delete(r))}}catch(e){s.e(e)}finally{s.f()}}}catch(e){o.e(e)}finally{o.f()}}this.store&&this.store.delete(e),this.reg.delete(e)}return this.cache&&this.cache.remove(e),this},u.clear=function(){var e,t=[],n=l(this.index.values());try{for(n.s();!(e=n.n()).done;){var r=e.value.clear();r.then&&t.push(r)}}catch(e){n.e(e)}finally{n.f()}if(this.tag){var i,o=l(this.tag.values());try{for(o.s();!(i=o.n()).done;){i.value.clear()}}catch(e){o.e(e)}finally{o.f()}}return this.store&&this.store.clear(),this.cache&&this.cache.clear(),t.length?Promise.all(t):this},u.contain=function(e){return this.db?this.index.get(this.field[0]).db.has(e):this.reg.has(e)},u.cleanup=function(){var e,t=l(this.index.values());try{for(t.s();!(e=t.n()).done;){e.value.cleanup()}}catch(e){t.e(e)}finally{t.f()}return this},u.get=function(e){return this.db?this.index.get(this.field[0]).db.enrich(e).then((function(e){return e[0]&&e[0].doc||null})):this.store.get(e)||null},u.set=function(e,t){return"object"===h(e)&&(e=y(t=e,this.key)),this.store.set(e,t),this},u.searchCache=E,u.export=function(e,t){var n,r,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;if(i2?n[0]:"";if(n=n.length>2?n[2]:n[1],this.worker&&r)return this.index.get(r).import(e);if(t){if("string"==typeof t&&(t=JSON.parse(t)),r)return this.index.get(r).import(n,t);switch(n){case"reg":this.fastupdate=!1,this.reg=Ee(t,this.reg);for(var i,o=0;o=0&&r.splice(u,1)}}else xe(this.map,e),this.depth&&xe(this.ctx,e);t||this.reg.delete(e)}return this.db&&(this.commit_task.push({del:e}),this.M&&Ie(this)),this.cache&&this.cache.remove(e),this};var Se={memory:{resolution:1},performance:{resolution:3,fastupdate:!0,context:{depth:1,resolution:1}},match:{tokenize:"forward"},score:{resolution:9,context:{depth:2,resolution:3}}};function Ce(e,t,n,r,i,o,u){var a,s;if(!(a=t[n])||u&&!a[u]){if(u?((t=a||(t[n]=f()))[u]=1,(a=(s=e.ctx).get(u))?s=a:s.set(u,s=e.keystore?new q(e.keystore):new Map)):(s=e.map,t[n]=1),(a=s.get(n))?s=a:s.set(n,s=a=[]),o)for(var c,h=0;h1?t+(r||0)<=e?n+(i||0):(e-1)/(t+(r||0))*(n+(i||0))+1|0:0}function Oe(e,t,n,r,i,o,u){var a=e.length,s=e;if(a>1)s=ce(e,t,n,r,i,o,u);else if(1===a)return u?Z.call(null,e[0],n,r):new ae(e[0],this);return u?s:new ae(s,this)}function ke(e,t,n,r,i,o,u){return e=Te(this,e,t,n,r,i,o,u),this.db?e.then((function(e){return i?e||[]:new ae(e,this)})):e&&e.length?i?Z.call(this,e,n,r):new ae(e,this):i?[]:new ae([],this)}function Be(e,t,n,r){var i=[];if(e&&e.length){if(e.length<=r)return void t.push(e);for(var o,u=0;un)&&(s=n,n=t,t=s),e.db?e.db.get(t,n,r,i,o,u,a):e=n?(e=e.ctx.get(n))&&e.get(t):e.map.get(t)}function Pe(e,t){if(!this||this.constructor!==Pe)return new Pe(e);if(e){var n=d(e)?e:e.preset;n&&(e=Object.assign({},Se[n],e))}else e={};var r=!0===(n=e.context)?{depth:1}:n||{},i=d(e.encoder)?N[e.encoder]:e.encode||e.encoder||{};this.encoder=i.encode?i:"object"===h(i)?new D(i):{encode:i},this.resolution=e.resolution||9,this.tokenize=n=(n=e.tokenize)&&"default"!==n&&"exact"!==n&&n||"strict",this.depth="strict"===n&&r.depth||0,this.bidirectional=!1!==r.bidirectional,this.fastupdate=!!e.fastupdate,this.score=e.score||null,(n=e.keystore||0)&&(this.keystore=n),this.map=n?new q(n):new Map,this.ctx=n?new q(n):new Map,this.reg=t||(this.fastupdate?n?new q(n):new Map:n?new L(n):new Set),this.N=r.resolution||3,this.rtl=i.rtl||e.rtl||!1,this.cache=(n=e.cache||null)&&new F(n),this.resolve=!1!==e.resolve,(n=e.db)&&(this.db=this.mount(n)),this.M=!1!==e.commit,this.commit_task=[],this.commit_timer=null,this.priority=e.priority||4}function Ie(e){e.commit_timer||(e.commit_timer=setTimeout((function(){e.commit_timer=null,e.db.commit(e)}),1))}Pe.prototype.add=function(e,t,n,r){if(t&&(e||0===e)){if(!r&&!n&&this.reg.has(e))return this.update(e,t);r=this.depth;var i=(t=this.encoder.encode(t,!r)).length;if(i){for(var o=f(),u=f(),a=this.resolution,s=0;s2){for(var p,y,v,b,g=1;g2){for(var m,w=0;ww;h--){d=c.substring(w,h),m=this.rtl?l-1-w:w;var D=this.score?this.score(t,c,s,d,m):je(a,i,s,l,m);Ce(this,u,d,D,e,n)}break}case"bidirectional":case"reverse":if(l>1){for(D=l-1;D>0;D--){d=c[this.rtl?l-1-D:D]+d;var A=this.score?this.score(t,c,s,d,D):je(a,i,s,l,D);Ce(this,u,d,A,e,n)}d=""}case"forward":if(l>1){for(D=0;D1&&sd)?d:c,this.score?this.score(t,d,s,c,D-1):je(l+(i/2>l?0:1),i,s,h-1,D-1),e,n,A?c:d)}}}}this.fastupdate||this.reg.add(e)}}return this.db&&(this.commit_task.push(n?{ins:e}:{del:e}),this.M&&Ie(this)),this},Pe.prototype.search=function(e,t,n){if(n||(t||"object"!==h(e)?"object"===h(t)&&(n=t,t=0):(n=e,e="")),n&&n.cache)return n.cache=!1,e=this.searchCache(e,t,n),n.cache=!0,e;var r,u,a,s,c,l,d,p,y=[],v=0;n&&(e=n.query||e,t=n.limit||t,v=n.offset||0,u=n.context,a=n.suggest,p=(s=n.resolve)&&n.enrich,l=n.boost,d=n.resolution,c=this.db&&n.tag),void 0===s&&(s=this.resolve),u=this.depth&&!1!==u;var b=this.encoder.encode(e,!u);if(r=b.length,t=t||(s?100:0),1===r)return ke.call(this,b[0],"",t,v,s,p,c);if(2===r&&u&&!a)return ke.call(this,b[1],b[0],t,v,s,p,c);var g,m,w,D=f(),A=0;if(u&&(g=b[0],A=1),d||0===d||(d=g?this.N:this.resolution),this.db){if(this.db.search&&!1!==(n=this.db.search(this,b,t,v,a,s,p,c)))return n;var E=this;return i(o().m((function e(){var n,i;return o().w((function(e){for(;;)switch(e.n){case 0:if(!(A2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;switch(o){case 0:n="reg",r=Ae(this.reg);break;case 1:n="cfg",r=null;break;case 2:n="map",r=ge(this.map,this.reg.size);break;case 3:n="ctx",r=we(this.ctx,this.reg.size);break;default:return}return Fe.call(this,e,t,n,r,i,o)},u.import=function(e,t){if(t)switch("string"==typeof t&&(t=JSON.parse(t)),e=e.split("."),"json"===e[e.length-1]&&e.pop(),3===e.length&&e.shift(),e=e.length>1?e[1]:e[0],e){case"reg":this.fastupdate=!1,this.reg=Ee(t,this.reg);break;case"map":this.map=me(t,this.map);break;case"ctx":this.ctx=De(t,this.ctx)}},u.serialize=function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],t="",n="",r="";if(this.reg.size){var i,o,u=l(this.reg.keys());try{for(u.s();!(o=u.n()).done;){var a=o.value;i||(i=h(a)),t+=(t?",":"")+("string"===i?'"'+a+'"':a)}}catch(e){u.e(e)}finally{u.f()}t="index.reg=new Set(["+t+"]);",n="index.map=new Map(["+(n=_e(this.map,i))+"]);";var s,c=l(this.ctx.entries());try{for(c.s();!(s=c.n()).done;){var f=s.value;a=f[0];var d=_e(f[1],i);r+=(r?",":"")+(d='["'+a+'",'+(d="new Map(["+d+"])")+"]")}}catch(e){c.e(e)}finally{c.f()}r="index.ctx=new Map(["+r+"]);"}return e?"function inject(index){"+t+n+r+"}":t+n+r},H(Pe.prototype);var Ne,Re="undefined"!=typeof window&&(window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB),Me=["map","ctx","tag","reg","cfg"],qe=f();function Le(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!this||this.constructor!==Le)return new Le(e,t);"object"===h(e)&&(t=e,e=e.name),this.id="flexsearch"+(e?":"+e.toLowerCase().replace(/[^a-z0-9_\-]/g,""):""),this.field=t.field?t.field.toLowerCase().replace(/[^a-z0-9_\-]/g,""):"",this.type=t.type,this.fastupdate=this.support_tag_search=!1,this.db=null,this.h={}}function Ke(e,t,n){for(var r,i,o=e.value,u=0,a=0;a=0){if(r=1,!(i.length>1)){o[a]=[];break}i.splice(s,1)}u+=i.length}if(n)break}u?r&&e.update(o):e.delete(),e.continue()}function $e(e,t){return new Promise((function(n,r){e.onsuccess=e.oncomplete=function(){t&&t(this.result),t=null,n(this.result)},e.onerror=e.onblocked=r,e=null}))}(u=Le.prototype).mount=function(e){return e.index?e.mount(this):(e.db=this,this.open())},u.open=function(){if(this.db)return this.db;var e=this;navigator.storage&&navigator.storage.persist&&navigator.storage.persist(),qe[e.id]||(qe[e.id]=[]),qe[e.id].push(e.field);var t=Re.open(e.id,1);return t.onupgradeneeded=function(){for(var t,n=e.db=this.result,r=0;r2&&void 0!==arguments[2]?arguments[2]:0,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,i=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],o=arguments.length>5&&void 0!==arguments[5]&&arguments[5];e=this.db.transaction((t?"ctx":"map")+(this.field?":"+this.field:""),"readonly").objectStore((t?"ctx":"map")+(this.field?":"+this.field:"")).get(t?t+":"+e:e);var u=this;return $e(e).then((function(e){var t=[];if(!e||!e.length)return t;if(i){if(!n&&!r&&1===e.length)return e[0];for(var a,s=0;s=a.length){r-=a.length;continue}for(var c=n?r+Math.min(a.length-r,n):a.length,l=r;l1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e=this.db.transaction("tag"+(this.field?":"+this.field:""),"readonly").objectStore("tag"+(this.field?":"+this.field:"")).get(e);var i=this;return $e(e).then((function(e){return!e||!e.length||n>=e.length?[]:t||n?(e=e.slice(n,n+t),r?i.enrich(e):e):e}))},u.enrich=function(e){"object"!==h(e)&&(e=[e]);for(var t=this.db.transaction("reg","readonly").objectStore("reg"),n=[],r=0;r0&&t-1 in e)}function C(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}x.fn=x.prototype={jquery:F,constructor:x,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return x.each(this,e)},map:function(e){return this.pushStack(x.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(x.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(x.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|"+B+")"+B+"*"),K=new RegExp(B+"|>"),$=new RegExp(I),U=new RegExp("^"+S+"$"),V={ID:new RegExp("^#("+S+")"),CLASS:new RegExp("^\\.("+S+")"),TAG:new RegExp("^("+S+"|[*])"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+I),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+B+"*(even|odd|(([+-]|)(\\d*)n|)"+B+"*(?:([+-]|)"+B+"*(\\d+)|))"+B+"*\\)|)","i"),bool:new RegExp("^(?:"+_+")$","i"),needsContext:new RegExp("^"+B+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+B+"*((?:-\\d)?\\d*)"+B+"*\\)|)(?=[^-]|$)","i")},H=/^(?:input|select|textarea|button)$/i,W=/^h\d$/i,z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/[+~]/,Y=new RegExp("\\\\[\\da-fA-F]{1,6}"+B+"?|\\\\([^\\r\\n\\f])","g"),X=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},J=function(){se()},Q=he((function(e){return!0===e.disabled&&C(e,"fieldset")}),{dir:"parentNode",next:"legend"});try{y.apply(u=s.call(N.childNodes),N.childNodes),u[N.childNodes.length].nodeType}catch(e){y={apply:function(e,t){R.apply(e,s.call(t))},call:function(e){R.apply(e,s.call(arguments,1))}}}function Z(e,t,n,r){var i,o,u,s,c,f,d,p=t&&t.ownerDocument,g=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==g&&9!==g&&11!==g)return n;if(!r&&(se(t),t=t||a,l)){if(11!==g&&(c=z.exec(e)))if(i=c[1]){if(9===g){if(!(u=t.getElementById(i)))return n;if(u.id===i)return y.call(n,u),n}else if(p&&(u=p.getElementById(i))&&Z.contains(t,u)&&u.id===i)return y.call(n,u),n}else{if(c[2])return y.apply(n,t.getElementsByTagName(e)),n;if((i=c[3])&&t.getElementsByClassName)return y.apply(n,t.getElementsByClassName(i)),n}if(!(E[e+" "]||h&&h.test(e))){if(d=e,p=t,1===g&&(K.test(e)||L.test(e))){for((p=G.test(e)&&ae(t.parentNode)||t)==t&&b.scope||((s=t.getAttribute("id"))?s=x.escapeSelector(s):t.setAttribute("id",s=v)),o=(f=le(e)).length;o--;)f[o]=(s?"#"+s:":scope")+" "+fe(f[o]);d=f.join(",")}try{return y.apply(n,p.querySelectorAll(d)),n}catch(t){E(e,!0)}finally{s===v&&t.removeAttribute("id")}}}return ge(e.replace(T,"$1"),t,n,r)}function ee(){var e=[];return function n(r,i){return e.push(r+" ")>t.cacheLength&&delete n[e.shift()],n[r+" "]=i}}function te(e){return e[v]=!0,e}function ne(e){var t=a.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function re(e){return function(t){return C(t,"input")&&t.type===e}}function ie(e){return function(t){return(C(t,"input")||C(t,"button"))&&t.type===e}}function oe(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&Q(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function ue(e){return te((function(t){return t=+t,te((function(n,r){for(var i,o=e([],n.length,t),u=o.length;u--;)n[i=o[u]]&&(n[i]=!(r[i]=n[i]))}))}))}function ae(e){return e&&void 0!==e.getElementsByTagName&&e}function se(e){var n,r=e?e.ownerDocument||e:N;return r!=a&&9===r.nodeType&&r.documentElement?(c=(a=r).documentElement,l=!x.isXMLDoc(a),d=c.matches||c.webkitMatchesSelector||c.msMatchesSelector,c.msMatchesSelector&&N!=a&&(n=a.defaultView)&&n.top!==n&&n.addEventListener("unload",J),b.getById=ne((function(e){return c.appendChild(e).id=x.expando,!a.getElementsByName||!a.getElementsByName(x.expando).length})),b.disconnectedMatch=ne((function(e){return d.call(e,"*")})),b.scope=ne((function(){return a.querySelectorAll(":scope")})),b.cssHas=ne((function(){try{return a.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}})),b.getById?(t.filter.ID=function(e){var t=e.replace(Y,X);return function(e){return e.getAttribute("id")===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&l){var n=t.getElementById(e);return n?[n]:[]}}):(t.filter.ID=function(e){var t=e.replace(Y,X);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&l){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];for(i=t.getElementsByName(e),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),t.find.TAG=function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},t.find.CLASS=function(e,t){if(void 0!==t.getElementsByClassName&&l)return t.getElementsByClassName(e)},h=[],ne((function(e){var t;c.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+B+"*(?:value|"+_+")"),e.querySelectorAll("[id~="+v+"-]").length||h.push("~="),e.querySelectorAll("a#"+v+"+*").length||h.push(".#.+[+~]"),e.querySelectorAll(":checked").length||h.push(":checked"),(t=a.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),c.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&h.push(":enabled",":disabled"),(t=a.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||h.push("\\["+B+"*name"+B+"*="+B+"*(?:''|\"\")")})),b.cssHas||h.push(":has"),h=h.length&&new RegExp(h.join("|")),F=function(e,t){if(e===t)return o=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!b.sortDetached&&t.compareDocumentPosition(e)===n?e===a||e.ownerDocument==N&&Z.contains(N,e)?-1:t===a||t.ownerDocument==N&&Z.contains(N,t)?1:i?f.call(i,e)-f.call(i,t):0:4&n?-1:1)},a):a}for(e in Z.matches=function(e,t){return Z(e,null,null,t)},Z.matchesSelector=function(e,t){if(se(e),l&&!E[t+" "]&&(!h||!h.test(t)))try{var n=d.call(e,t);if(n||b.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){E(t,!0)}return Z(t,a,null,[e]).length>0},Z.contains=function(e,t){return(e.ownerDocument||e)!=a&&se(e),x.contains(e,t)},Z.attr=function(e,n){(e.ownerDocument||e)!=a&&se(e);var r=t.attrHandle[n.toLowerCase()],i=r&&p.call(t.attrHandle,n.toLowerCase())?r(e,n,!l):void 0;return void 0!==i?i:e.getAttribute(n)},Z.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},x.uniqueSort=function(e){var t,n=[],r=0,u=0;if(o=!b.sortStable,i=!b.sortStable&&s.call(e,0),O.call(e,F),o){for(;t=e[u++];)t===e[u]&&(r=n.push(u));for(;r--;)k.call(e,n[r],1)}return i=null,e},x.fn.uniqueSort=function(){return this.pushStack(x.uniqueSort(s.apply(this)))},t=x.expr={cacheLength:50,createPseudo:te,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Y,X),e[3]=(e[3]||e[4]||e[5]||"").replace(Y,X),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||Z.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&Z.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&$.test(n)&&(t=le(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Y,X).toLowerCase();return"*"===e?function(){return!0}:function(e){return C(e,t)}},CLASS:function(e){var t=w[e+" "];return t||(t=new RegExp("(^|"+B+")"+e+"("+B+"|$)"))&&w(e,(function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")}))},ATTR:function(e,t,n){return function(r){var i=Z.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(M," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),u="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,s){var c,l,f,h,d,p=o!==u?"nextSibling":"previousSibling",y=t.parentNode,b=a&&t.nodeName.toLowerCase(),m=!s&&!a,w=!1;if(y){if(o){for(;p;){for(f=t;f=f[p];)if(a?C(f,b):1===f.nodeType)return!1;d=p="only"===e&&!d&&"nextSibling"}return!0}if(d=[u?y.firstChild:y.lastChild],u&&m){for(w=(h=(c=(l=y[v]||(y[v]={}))[e]||[])[0]===g&&c[1])&&c[2],f=h&&y.childNodes[h];f=++h&&f&&f[p]||(w=h=0)||d.pop();)if(1===f.nodeType&&++w&&f===t){l[e]=[g,h,w];break}}else if(m&&(w=h=(c=(l=t[v]||(t[v]={}))[e]||[])[0]===g&&c[1]),!1===w)for(;(f=++h&&f&&f[p]||(w=h=0)||d.pop())&&(!(a?C(f,b):1===f.nodeType)||!++w||(m&&((l=f[v]||(f[v]={}))[e]=[g,w]),f!==t)););return(w-=i)===r||w%r==0&&w/r>=0}}},PSEUDO:function(e,n){var r,i=t.pseudos[e]||t.setFilters[e.toLowerCase()]||Z.error("unsupported pseudo: "+e);return i[v]?i(n):i.length>1?(r=[e,e,"",n],t.setFilters.hasOwnProperty(e.toLowerCase())?te((function(e,t){for(var r,o=i(e,n),u=o.length;u--;)e[r=f.call(e,o[u])]=!(t[r]=o[u])})):function(e){return i(e,0,r)}):i}},pseudos:{not:te((function(e){var t=[],n=[],r=be(e.replace(T,"$1"));return r[v]?te((function(e,t,n,i){for(var o,u=r(e,null,i,[]),a=e.length;a--;)(o=u[a])&&(e[a]=!(t[a]=o))})):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}})),has:te((function(e){return function(t){return Z(e,t).length>0}})),contains:te((function(e){return e=e.replace(Y,X),function(t){return(t.textContent||x.text(t)).indexOf(e)>-1}})),lang:te((function(e){return U.test(e||"")||Z.error("unsupported lang: "+e),e=e.replace(Y,X).toLowerCase(),function(t){var n;do{if(n=l?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}})),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===c},focus:function(e){return e===function(){try{return a.activeElement}catch(e){}}()&&a.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:oe(!1),disabled:oe(!0),checked:function(e){return C(e,"input")&&!!e.checked||C(e,"option")&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!t.pseudos.empty(e)},header:function(e){return W.test(e.nodeName)},input:function(e){return H.test(e.nodeName)},button:function(e){return C(e,"input")&&"button"===e.type||C(e,"button")},text:function(e){var t;return C(e,"input")&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ue((function(){return[0]})),last:ue((function(e,t){return[t-1]})),eq:ue((function(e,t,n){return[n<0?n+t:n]})),even:ue((function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e})),gt:ue((function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function pe(e,t,n,r,i){for(var o,u=[],a=0,s=e.length,c=null!=t;a-1&&(o[c]=!(u[c]=h))}}else d=pe(d===u?d.splice(b,d.length):d),i?i(null,u,d,s):y.apply(u,d)}))}function ve(e){for(var n,i,o,u=e.length,a=t.relative[e[0].type],s=a||t.relative[" "],c=a?1:0,l=he((function(e){return e===n}),s,!0),h=he((function(e){return f.call(n,e)>-1}),s,!0),d=[function(e,t,i){var o=!a&&(i||t!=r)||((n=t).nodeType?l(e,t,i):h(e,t,i));return n=null,o}];c1&&de(d),c>1&&fe(e.slice(0,c-1).concat({value:" "===e[c-2].type?"*":""})).replace(T,"$1"),i,c0,o=e.length>0,u=function(u,s,c,f,h){var d,p,v,b=0,m="0",w=u&&[],D=[],A=r,E=u||o&&t.find.TAG("*",h),F=g+=null==A?1:Math.random()||.1,_=E.length;for(h&&(r=s==a||s||h);m!==_&&null!=(d=E[m]);m++){if(o&&d){for(p=0,s||d.ownerDocument==a||(se(d),c=!l);v=e[p++];)if(v(d,s||a,c)){y.call(f,d);break}h&&(g=F)}i&&((d=!v&&d)&&b--,u&&w.push(d))}if(b+=m,i&&m!==b){for(p=0;v=n[p++];)v(w,D,s,c);if(u){if(b>0)for(;m--;)w[m]||D[m]||(D[m]=j.call(f));D=pe(D)}y.apply(f,D),h&&!u&&D.length>0&&b+n.length>1&&x.uniqueSort(f)}return h&&(g=F,r=A),w};return i?te(u):u}(u,o)),s.selector=e}return s}function ge(e,n,r,i){var o,u,a,s,c,f="function"==typeof e&&e,h=!i&&le(e=f.selector||e);if(r=r||[],1===h.length){if((u=h[0]=h[0].slice(0)).length>2&&"ID"===(a=u[0]).type&&9===n.nodeType&&l&&t.relative[u[1].type]){if(!(n=(t.find.ID(a.matches[0].replace(Y,X),n)||[])[0]))return r;f&&(n=n.parentNode),e=e.slice(u.shift().value.length)}for(o=V.needsContext.test(e)?0:u.length;o--&&(a=u[o],!t.relative[s=a.type]);)if((c=t.find[s])&&(i=c(a.matches[0].replace(Y,X),G.test(u[0].type)&&ae(n.parentNode)||n))){if(u.splice(o,1),!(e=i.length&&fe(u)))return y.apply(r,i),r;break}}return(f||be(e,h))(i,n,!l,r,!n||G.test(e)&&ae(n.parentNode)||n),r}ce.prototype=t.filters=t.pseudos,t.setFilters=new ce,b.sortStable=v.split("").sort(F).join("")===v,se(),b.sortDetached=ne((function(e){return 1&e.compareDocumentPosition(a.createElement("fieldset"))})),x.find=Z,x.expr[":"]=x.expr.pseudos,x.unique=x.uniqueSort,Z.compile=be,Z.select=ge,Z.setDocument=se,Z.tokenize=le,Z.escape=x.escapeSelector,Z.getText=x.text,Z.isXML=x.isXMLDoc,Z.selectors=x.expr,Z.support=x.support,Z.uniqueSort=x.uniqueSort}();var M=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},q=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},L=x.expr.match.needsContext,K=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function $(e,t,n){return g(t)?x.grep(e,(function(e,r){return!!t.call(e,r,e)!==n})):t.nodeType?x.grep(e,(function(e){return e===t!==n})):"string"!=typeof t?x.grep(e,(function(e){return f.call(t,e)>-1!==n})):x.filter(t,e,n)}x.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,(function(e){return 1===e.nodeType})))},x.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(x(e).filter((function(){for(t=0;t1?x.uniqueSort(n):n},filter:function(e){return this.pushStack($(this,e||[],!1))},not:function(e){return this.pushStack($(this,e||[],!0))},is:function(e){return!!$(this,"string"==typeof e&&L.test(e)?x(e):e||[],!1).length}});var U,V=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(x.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||U,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:V.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:w,!0)),K.test(r[1])&&x.isPlainObject(t))for(r in t)g(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=w.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(x):x.makeArray(e,this)}).prototype=x.fn,U=x(w);var H=/^(?:parents|prev(?:Until|All))/,W={children:!0,contents:!0,next:!0,prev:!0};function z(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}x.fn.extend({has:function(e){var t=x(e,this),n=t.length;return this.filter((function(){for(var e=0;e-1:1===n.nodeType&&x.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?x.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?f.call(x(e),this[0]):f.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(x.uniqueSort(x.merge(this.get(),x(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return M(e,"parentNode")},parentsUntil:function(e,t,n){return M(e,"parentNode",n)},next:function(e){return z(e,"nextSibling")},prev:function(e){return z(e,"previousSibling")},nextAll:function(e){return M(e,"nextSibling")},prevAll:function(e){return M(e,"previousSibling")},nextUntil:function(e,t,n){return M(e,"nextSibling",n)},prevUntil:function(e,t,n){return M(e,"previousSibling",n)},siblings:function(e){return q((e.parentNode||{}).firstChild,e)},children:function(e){return q(e.firstChild)},contents:function(e){return null!=e.contentDocument&&a(e.contentDocument)?e.contentDocument:(C(e,"template")&&(e=e.content||e),x.merge([],e.childNodes))}},(function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(W[e]||x.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}}));var G=/[^\x20\t\r\n\f]+/g;function Y(e){return e}function X(e){throw e}function J(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}x.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return x.each(e.match(G)||[],(function(e,n){t[n]=!0})),t}(e):x.extend({},e);var t,n,r,i,o=[],u=[],a=-1,s=function(){for(i=i||e.once,r=t=!0;u.length;a=-1)for(n=u.shift();++a-1;)o.splice(n,1),n<=a&&a--})),this},has:function(e){return e?x.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=u=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=u=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],u.push(n),t||s()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},x.extend({Deferred:function(e){var t=[["notify","progress",x.Callbacks("memory"),x.Callbacks("memory"),2],["resolve","done",x.Callbacks("once memory"),x.Callbacks("once memory"),0,"resolved"],["reject","fail",x.Callbacks("once memory"),x.Callbacks("once memory"),1,"rejected"]],r="pending",o={state:function(){return r},always:function(){return u.done(arguments).fail(arguments),this},catch:function(e){return o.then(null,e)},pipe:function(){var e=arguments;return x.Deferred((function(n){x.each(t,(function(t,r){var i=g(e[r[4]])&&e[r[4]];u[r[1]]((function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[r[0]+"With"](this,i?[e]:arguments)}))})),e=null})).promise()},then:function(e,r,o){var u=0;function a(e,t,r,o){return function(){var s=this,c=arguments,l=function(){var n,l;if(!(e=u&&(r!==X&&(s=void 0,c=[n]),t.rejectWith(s,c))}};e?f():(x.Deferred.getErrorHook?f.error=x.Deferred.getErrorHook():x.Deferred.getStackHook&&(f.error=x.Deferred.getStackHook()),n.setTimeout(f))}}return x.Deferred((function(n){t[0][3].add(a(0,n,g(o)?o:Y,n.notifyWith)),t[1][3].add(a(0,n,g(e)?e:Y)),t[2][3].add(a(0,n,g(r)?r:X))})).promise()},promise:function(e){return null!=e?x.extend(e,o):o}},u={};return x.each(t,(function(e,n){var i=n[2],a=n[5];o[n[1]]=i.add,a&&i.add((function(){r=a}),t[3-e][2].disable,t[3-e][3].disable,t[0][2].lock,t[0][3].lock),i.add(n[3].fire),u[n[0]]=function(){return u[n[0]+"With"](this===u?void 0:this,arguments),this},u[n[0]+"With"]=i.fireWith})),o.promise(u),e&&e.call(u,u),u},when:function(e){var t=arguments.length,n=t,r=Array(n),i=s.call(arguments),o=x.Deferred(),u=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?s.call(arguments):n,--t||o.resolveWith(r,i)}};if(t<=1&&(J(e,o.done(u(n)).resolve,o.reject,!t),"pending"===o.state()||g(i[n]&&i[n].then)))return o.then();for(;n--;)J(i[n],u(n),o.reject);return o.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;x.Deferred.exceptionHook=function(e,t){n.console&&n.console.warn&&e&&Q.test(e.name)&&n.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},x.readyException=function(e){n.setTimeout((function(){throw e}))};var Z=x.Deferred();function ee(){w.removeEventListener("DOMContentLoaded",ee),n.removeEventListener("load",ee),x.ready()}x.fn.ready=function(e){return Z.then(e).catch((function(e){x.readyException(e)})),this},x.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--x.readyWait:x.isReady)||(x.isReady=!0,!0!==e&&--x.readyWait>0||Z.resolveWith(w,[x]))}}),x.ready.then=Z.then,"complete"===w.readyState||"loading"!==w.readyState&&!w.documentElement.doScroll?n.setTimeout(x.ready):(w.addEventListener("DOMContentLoaded",ee),n.addEventListener("load",ee));var te=function(e,t,n,r,i,o,u){var a=0,s=e.length,c=null==n;if("object"===E(n))for(a in i=!0,n)te(e,t,a,n[a],!0,o,u);else if(void 0!==r&&(i=!0,g(r)||(u=!0),c&&(u?(t.call(e,r),t=null):(c=t,t=function(e,t,n){return c.call(x(e),n)})),t))for(;a1,null,!0)},removeData:function(e){return this.each((function(){ce.remove(this,e)}))}}),x.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=se.get(e,t),n&&(!r||Array.isArray(n)?r=se.access(e,t,x.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,(function(){x.dequeue(e,t)}),o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return se.get(e,n)||se.access(e,n,{empty:x.Callbacks("once memory").add((function(){se.remove(e,[t+"queue",n])}))})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;Fe=w.createDocumentFragment().appendChild(w.createElement("div")),(_e=w.createElement("input")).setAttribute("type","radio"),_e.setAttribute("checked","checked"),_e.setAttribute("name","t"),Fe.appendChild(_e),b.checkClone=Fe.cloneNode(!0).cloneNode(!0).lastChild.checked,Fe.innerHTML="",b.noCloneChecked=!!Fe.cloneNode(!0).lastChild.defaultValue,Fe.innerHTML="",b.option=!!Fe.lastChild;var je={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Oe(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&C(e,t)?x.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Be=/<|&#?\w+;/;function Te(e,t,n,r,i){for(var o,u,a,s,c,l,f=t.createDocumentFragment(),h=[],d=0,p=e.length;d-1)i&&i.push(o);else if(c=be(o),u=Oe(f.appendChild(o),"script"),c&&ke(u),n)for(l=0;o=u[l++];)Ce.test(o.type||"")&&n.push(o);return f}var Pe=/^([^.]*)(?:\.(.+)|)/;function Ie(){return!0}function Ne(){return!1}function Re(e,t,n,r,o,u){var a,s;if("object"===i(t)){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Re(e,s,n,r,t[s],u);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),!1===o)o=Ne;else if(!o)return e;return 1===u&&(a=o,o=function(e){return x().off(e),a.apply(this,arguments)},o.guid=a.guid||(a.guid=x.guid++)),e.each((function(){x.event.add(this,t,o,r,n)}))}function Me(e,t,n){n?(se.set(e,t,!1),x.event.add(e,t,{namespace:!1,handler:function(e){var n,r=se.get(this,t);if(1&e.isTrigger&&this[t]){if(r)(x.event.special[t]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),se.set(this,t,r),this[t](),n=se.get(this,t),se.set(this,t,!1),r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n}else r&&(se.set(this,t,x.event.trigger(r[0],r.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Ie)}})):void 0===se.get(e,t)&&x.event.add(e,t,Ie)}x.event={global:{},add:function(e,t,n,r,i){var o,u,a,s,c,l,f,h,d,p,y,v=se.get(e);if(ue(e))for(n.handler&&(n=(o=n).handler,i=o.selector),i&&x.find.matchesSelector(ve,i),n.guid||(n.guid=x.guid++),(s=v.events)||(s=v.events=Object.create(null)),(u=v.handle)||(u=v.handle=function(t){return void 0!==x&&x.event.triggered!==t.type?x.event.dispatch.apply(e,arguments):void 0}),c=(t=(t||"").match(G)||[""]).length;c--;)d=y=(a=Pe.exec(t[c])||[])[1],p=(a[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},l=x.extend({type:d,origType:y,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&x.expr.match.needsContext.test(i),namespace:p.join(".")},o),(h=s[d])||((h=s[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,p,u)||e.addEventListener&&e.addEventListener(d,u)),f.add&&(f.add.call(e,l),l.handler.guid||(l.handler.guid=n.guid)),i?h.splice(h.delegateCount++,0,l):h.push(l),x.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,u,a,s,c,l,f,h,d,p,y,v=se.hasData(e)&&se.get(e);if(v&&(s=v.events)){for(c=(t=(t||"").match(G)||[""]).length;c--;)if(d=y=(a=Pe.exec(t[c])||[])[1],p=(a[2]||"").split(".").sort(),d){for(f=x.event.special[d]||{},h=s[d=(r?f.delegateType:f.bindType)||d]||[],a=a[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=h.length;o--;)l=h[o],!i&&y!==l.origType||n&&n.guid!==l.guid||a&&!a.test(l.namespace)||r&&r!==l.selector&&("**"!==r||!l.selector)||(h.splice(o,1),l.selector&&h.delegateCount--,f.remove&&f.remove.call(e,l));u&&!h.length&&(f.teardown&&!1!==f.teardown.call(e,p,v.handle)||x.removeEvent(e,d,v.handle),delete s[d])}else for(d in s)x.event.remove(e,d+t[c],n,r,!0);x.isEmptyObject(s)&&se.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,u,a=new Array(arguments.length),s=x.event.fix(e),c=(se.get(this,"events")||Object.create(null))[s.type]||[],l=x.event.special[s.type]||{};for(a[0]=s,t=1;t=1))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==e.type||!0!==c.disabled)){for(o=[],u={},n=0;n-1:x.find(i,this,null,[c]).length),u[i]&&o.push(r);o.length&&a.push({elem:c,handlers:o})}return c=this,s\s*$/g;function $e(e,t){return C(e,"table")&&C(11!==t.nodeType?t:t.firstChild,"tr")&&x(e).children("tbody")[0]||e}function Ue(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ve(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function He(e,t){var n,r,i,o,u,a;if(1===t.nodeType){if(se.hasData(e)&&(a=se.get(e).events))for(i in se.remove(t,"handle events"),a)for(n=0,r=a[i].length;n1&&"string"==typeof p&&!b.checkClone&&Le.test(p))return e.each((function(i){var o=e.eq(i);y&&(t[0]=p.call(this,i,o.html())),ze(o,t,n,r)}));if(h&&(o=(i=Te(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(a=(u=x.map(Oe(i,"script"),Ue)).length;f0&&ke(u,!s&&Oe(e,"script")),a},cleanData:function(e){for(var t,n,r,i=x.event.special,o=0;void 0!==(n=e[o]);o++)if(ue(n)){if(t=n[se.expando]){if(t.events)for(r in t.events)i[r]?x.event.remove(n,r):x.removeEvent(n,r,t.handle);n[se.expando]=void 0}n[ce.expando]&&(n[ce.expando]=void 0)}}}),x.fn.extend({detach:function(e){return Ge(this,e,!0)},remove:function(e){return Ge(this,e)},text:function(e){return te(this,(function(e){return void 0===e?x.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return ze(this,arguments,(function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||$e(this,e).appendChild(e)}))},prepend:function(){return ze(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=$e(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return ze(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return ze(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(Oe(e,!1)),e.textContent="");return this},clone:function(e,t){return e=e??!1,t=t??e,this.map((function(){return x.clone(this,e,t)}))},html:function(e){return te(this,(function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!qe.test(e)&&!je[(Se.exec(e)||["",""])[1].toLowerCase()]){e=x.htmlPrefilter(e);try{for(;n=0&&(s+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-s-a-.5))||0),s+c}function ft(e,t,n){var r=Je(e),i=(!b.boxSizingReliable()||n)&&"border-box"===x.css(e,"boxSizing",!1,r),o=i,u=et(e,t,r),a="offset"+t[0].toUpperCase()+t.slice(1);if(Ye.test(u)){if(!n)return u;u="auto"}return(!b.boxSizingReliable()&&i||!b.reliableTrDimensions()&&C(e,"tr")||"auto"===u||!parseFloat(u)&&"inline"===x.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===x.css(e,"boxSizing",!1,r),(o=a in e)&&(u=e[a])),(u=parseFloat(u)||0)+lt(e,t,n||(i?"border":"content"),o,r,u)+"px"}function ht(e,t,n,r,i){return new ht.prototype.init(e,t,n,r,i)}x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=et(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,u,a,s=oe(t),c=Xe.test(t),l=e.style;if(c||(t=ot(s)),a=x.cssHooks[t]||x.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(o=a.get(e,!1,r))?o:l[t];"string"===(u=i(n))&&(o=pe.exec(n))&&o[1]&&(n=we(e,t,o),u="number"),null!=n&&n==n&&("number"!==u||c||(n+=o&&o[3]||(x.cssNumber[s]?"":"px")),b.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(c?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,u,a=oe(t);return Xe.test(t)||(t=ot(a)),(u=x.cssHooks[t]||x.cssHooks[a])&&"get"in u&&(i=u.get(e,!0,n)),void 0===i&&(i=et(e,t,r)),"normal"===i&&t in st&&(i=st[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),x.each(["height","width"],(function(e,t){x.cssHooks[t]={get:function(e,n,r){if(n)return!ut.test(x.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?ft(e,t,r):Qe(e,at,(function(){return ft(e,t,r)}))},set:function(e,n,r){var i,o=Je(e),u=!b.scrollboxSize()&&"absolute"===o.position,a=(u||r)&&"border-box"===x.css(e,"boxSizing",!1,o),s=r?lt(e,t,r,a,o):0;return a&&u&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-lt(e,t,"border",!1,o)-.5)),s&&(i=pe.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=x.css(e,t)),ct(0,n,s)}}})),x.cssHooks.marginLeft=tt(b.reliableMarginLeft,(function(e,t){if(t)return(parseFloat(et(e,"marginLeft"))||e.getBoundingClientRect().left-Qe(e,{marginLeft:0},(function(){return e.getBoundingClientRect().left})))+"px"})),x.each({margin:"",padding:"",border:"Width"},(function(e,t){x.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+ye[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(x.cssHooks[e+t].set=ct)})),x.fn.extend({css:function(e,t){return te(this,(function(e,t,n){var r,i,o={},u=0;if(Array.isArray(t)){for(r=Je(e),i=t.length;u1)}}),x.Tween=ht,ht.prototype={constructor:ht,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||x.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=ht.propHooks[this.prop];return e&&e.get?e.get(this):ht.propHooks._default.get(this)},run:function(e){var t,n=ht.propHooks[this.prop];return this.options.duration?this.pos=t=x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ht.propHooks._default.set(this),this}},ht.prototype.init.prototype=ht.prototype,ht.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=x.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):1!==e.elem.nodeType||!x.cssHooks[e.prop]&&null==e.elem.style[ot(e.prop)]?e.elem[e.prop]=e.now:x.style(e.elem,e.prop,e.now+e.unit)}}},ht.propHooks.scrollTop=ht.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},x.fx=ht.prototype.init,x.fx.step={};var dt,pt,yt=/^(?:toggle|show|hide)$/,vt=/queueHooks$/;function bt(){pt&&(!1===w.hidden&&n.requestAnimationFrame?n.requestAnimationFrame(bt):n.setTimeout(bt,x.fx.interval),x.fx.tick())}function gt(){return n.setTimeout((function(){dt=void 0})),dt=Date.now()}function mt(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ye[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function wt(e,t,n){for(var r,i=(Dt.tweeners[t]||[]).concat(Dt.tweeners["*"]),o=0,u=i.length;o1)},removeAttr:function(e){return this.each((function(){x.removeAttr(this,e)}))}}),x.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?x.prop(e,t,n):(1===o&&x.isXMLDoc(e)||(i=x.attrHooks[t.toLowerCase()]||(x.expr.match.bool.test(t)?At:void 0)),void 0!==n?null===n?void x.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:(r=x.find.attr(e,t))??void 0)},attrHooks:{type:{set:function(e,t){if(!b.radioValue&&"radio"===t&&C(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(G);if(i&&1===e.nodeType)for(;n=i[r++];)e.removeAttribute(n)}}),At={set:function(e,t,n){return!1===t?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),(function(e,t){var n=Et[t]||x.find.attr;Et[t]=function(e,t,r){var i,o,u=t.toLowerCase();return r||(o=Et[u],Et[u]=i,i=null!=n(e,t,r)?u:null,Et[u]=o),i}}));var Ft=/^(?:input|select|textarea|button)$/i,_t=/^(?:a|area)$/i;function xt(e){return(e.match(G)||[]).join(" ")}function St(e){return e.getAttribute&&e.getAttribute("class")||""}function Ct(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(G)||[]}x.fn.extend({prop:function(e,t){return te(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each((function(){delete this[x.propFix[e]||e]}))}}),x.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&x.isXMLDoc(e)||(t=x.propFix[t]||t,i=x.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Ft.test(e.nodeName)||_t.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),b.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){x.propFix[this.toLowerCase()]=this})),x.fn.extend({addClass:function(e){var t,n,r,i,o,u;return g(e)?this.each((function(t){x(this).addClass(e.call(this,t,St(this)))})):(t=Ct(e)).length?this.each((function(){if(r=St(this),n=1===this.nodeType&&" "+xt(r)+" "){for(o=0;o-1;)n=n.replace(" "+i+" "," ");u=xt(n),r!==u&&this.setAttribute("class",u)}})):this:this.attr("class","")},toggleClass:function(e,t){var n,r,o,u,a=i(e),s="string"===a||Array.isArray(e);return g(e)?this.each((function(n){x(this).toggleClass(e.call(this,n,St(this),t),t)})):"boolean"==typeof t&&s?t?this.addClass(e):this.removeClass(e):(n=Ct(e),this.each((function(){if(s)for(u=x(this),o=0;o-1)return!0;return!1}});var jt=/\r/g;x.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=g(e),this.each((function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,x(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=x.map(i,(function(e){return null==e?"":e+""}))),(t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))}))):i?(t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(jt,""):n??"":void 0}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:xt(x.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,u="select-one"===e.type,a=u?null:[],s=u?o+1:i.length;for(r=o<0?s:u?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),x.each(["radio","checkbox"],(function(){x.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=x.inArray(x(e).val(),t)>-1}},b.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}));var Ot=n.location,kt={guid:Date.now()},Bt=/\?/;x.parseXML=function(e){var t,r;if(!e||"string"!=typeof e)return null;try{t=(new n.DOMParser).parseFromString(e,"text/xml")}catch(e){}return r=t&&t.getElementsByTagName("parsererror")[0],t&&!r||x.error("Invalid XML: "+(r?x.map(r.childNodes,(function(e){return e.textContent})).join("\n"):e)),t};var Tt=/^(?:focusinfocus|focusoutblur)$/,Pt=function(e){e.stopPropagation()};x.extend(x.event,{trigger:function(e,t,r,o){var u,a,s,c,l,f,h,d,y=[r||w],v=p.call(e,"type")?e.type:e,b=p.call(e,"namespace")?e.namespace.split("."):[];if(a=d=s=r=r||w,3!==r.nodeType&&8!==r.nodeType&&!Tt.test(v+x.event.triggered)&&(v.indexOf(".")>-1&&(b=v.split("."),v=b.shift(),b.sort()),l=v.indexOf(":")<0&&"on"+v,(e=e[x.expando]?e:new x.Event(v,"object"===i(e)&&e)).isTrigger=o?2:3,e.namespace=b.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),t=null==t?[e]:x.makeArray(t,[e]),h=x.event.special[v]||{},o||!h.trigger||!1!==h.trigger.apply(r,t))){if(!o&&!h.noBubble&&!m(r)){for(c=h.delegateType||v,Tt.test(c+v)||(a=a.parentNode);a;a=a.parentNode)y.push(a),s=a;s===(r.ownerDocument||w)&&y.push(s.defaultView||s.parentWindow||n)}for(u=0;(a=y[u++])&&!e.isPropagationStopped();)d=a,e.type=u>1?c:h.bindType||v,(f=(se.get(a,"events")||Object.create(null))[e.type]&&se.get(a,"handle"))&&f.apply(a,t),(f=l&&a[l])&&f.apply&&ue(a)&&(e.result=f.apply(a,t),!1===e.result&&e.preventDefault());return e.type=v,o||e.isDefaultPrevented()||h._default&&!1!==h._default.apply(y.pop(),t)||!ue(r)||l&&g(r[v])&&!m(r)&&((s=r[l])&&(r[l]=null),x.event.triggered=v,e.isPropagationStopped()&&d.addEventListener(v,Pt),r[v](),e.isPropagationStopped()&&d.removeEventListener(v,Pt),x.event.triggered=void 0,s&&(r[l]=s)),e.result}},simulate:function(e,t,n){var r=x.extend(new x.Event,n,{type:e,isSimulated:!0});x.event.trigger(r,null,t)}}),x.fn.extend({trigger:function(e,t){return this.each((function(){x.event.trigger(e,t,this)}))},triggerHandler:function(e,t){var n=this[0];if(n)return x.event.trigger(e,t,n,!0)}});var It=/\[\]$/,Nt=/\r?\n/g,Rt=/^(?:submit|button|image|reset|file)$/i,Mt=/^(?:input|select|textarea|keygen)/i;function qt(e,t,n,r){var o;if(Array.isArray(t))x.each(t,(function(t,o){n||It.test(e)?r(e,o):qt(e+"["+("object"===i(o)&&null!=o?t:"")+"]",o,n,r)}));else if(n||"object"!==E(t))r(e,t);else for(o in t)qt(e+"["+o+"]",t[o],n,r)}x.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(n??"")};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,(function(){i(this.name,this.value)}));else for(n in e)qt(n,e[n],t,i);return r.join("&")},x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this})).filter((function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&Mt.test(this.nodeName)&&!Rt.test(e)&&(this.checked||!xe.test(e))})).map((function(e,t){var n=x(this).val();return null==n?null:Array.isArray(n)?x.map(n,(function(e){return{name:t.name,value:e.replace(Nt,"\r\n")}})):{name:t.name,value:n.replace(Nt,"\r\n")}})).get()}});var Lt=/%20/g,Kt=/#.*$/,$t=/([?&])_=[^&]*/,Ut=/^(.*?):[ \t]*([^\r\n]*)$/gm,Vt=/^(?:GET|HEAD)$/,Ht=/^\/\//,Wt={},zt={},Gt="*/".concat("*"),Yt=w.createElement("a");function Xt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(G)||[];if(g(n))for(;r=o[i++];)"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Jt(e,t,n,r){var i={},o=e===zt;function u(a){var s;return i[a]=!0,x.each(e[a]||[],(function(e,a){var c=a(t,n,r);return"string"!=typeof c||o||i[c]?o?!(s=c):void 0:(t.dataTypes.unshift(c),u(c),!1)})),s}return u(t.dataTypes[0])||!i["*"]&&u("*")}function Qt(e,t){var n,r,i=x.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&x.extend(!0,e,r),e}Yt.href=Ot.href,x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ot.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Ot.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Gt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Qt(Qt(e,x.ajaxSettings),t):Qt(x.ajaxSettings,e)},ajaxPrefilter:Xt(Wt),ajaxTransport:Xt(zt),ajax:function(e,t){"object"===i(e)&&(t=e,e=void 0),t=t||{};var r,o,u,a,s,c,l,f,h,d,p=x.ajaxSetup({},t),y=p.context||p,v=p.context&&(y.nodeType||y.jquery)?x(y):x.event,b=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},D={},A={},E="canceled",F={readyState:0,getResponseHeader:function(e){var t;if(l){if(!a)for(a={};t=Ut.exec(u);)a[t[1].toLowerCase()+" "]=(a[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=a[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return l?u:null},setRequestHeader:function(e,t){return null==l&&(e=A[e.toLowerCase()]=A[e.toLowerCase()]||e,D[e]=t),this},overrideMimeType:function(e){return null==l&&(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)F.always(e[F.status]);else for(t in e)m[t]=[m[t],e[t]];return this},abort:function(e){var t=e||E;return r&&r.abort(t),_(0,t),this}};if(b.promise(F),p.url=((e||p.url||Ot.href)+"").replace(Ht,Ot.protocol+"//"),p.type=t.method||t.type||p.method||p.type,p.dataTypes=(p.dataType||"*").toLowerCase().match(G)||[""],null==p.crossDomain){c=w.createElement("a");try{c.href=p.url,c.href=c.href,p.crossDomain=Yt.protocol+"//"+Yt.host!=c.protocol+"//"+c.host}catch(e){p.crossDomain=!0}}if(p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),Jt(Wt,p,t,F),l)return F;for(h in(f=x.event&&p.global)&&0==x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Vt.test(p.type),o=p.url.replace(Kt,""),p.hasContent?p.data&&p.processData&&0===(p.contentType||"").indexOf("application/x-www-form-urlencoded")&&(p.data=p.data.replace(Lt,"+")):(d=p.url.slice(o.length),p.data&&(p.processData||"string"==typeof p.data)&&(o+=(Bt.test(o)?"&":"?")+p.data,delete p.data),!1===p.cache&&(o=o.replace($t,"$1"),d=(Bt.test(o)?"&":"?")+"_="+kt.guid+++d),p.url=o+d),p.ifModified&&(x.lastModified[o]&&F.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&F.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&!1!==p.contentType||t.contentType)&&F.setRequestHeader("Content-Type",p.contentType),F.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Gt+"; q=0.01":""):p.accepts["*"]),p.headers)F.setRequestHeader(h,p.headers[h]);if(p.beforeSend&&(!1===p.beforeSend.call(y,F,p)||l))return F.abort();if(E="abort",g.add(p.complete),F.done(p.success),F.fail(p.error),r=Jt(zt,p,t,F)){if(F.readyState=1,f&&v.trigger("ajaxSend",[F,p]),l)return F;p.async&&p.timeout>0&&(s=n.setTimeout((function(){F.abort("timeout")}),p.timeout));try{l=!1,r.send(D,_)}catch(e){if(l)throw e;_(-1,e)}}else _(-1,"No Transport");function _(e,t,i,a){var c,h,d,w,D,A=t;l||(l=!0,s&&n.clearTimeout(s),r=void 0,u=a||"",F.readyState=e>0?4:0,c=e>=200&&e<300||304===e,i&&(w=function(e,t,n){for(var r,i,o,u,a=e.contents,s=e.dataTypes;"*"===s[0];)s.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in a)if(a[i]&&a[i].test(r)){s.unshift(i);break}if(s[0]in n)o=s[0];else{for(i in n){if(!s[0]||e.converters[i+" "+s[0]]){o=i;break}u||(u=i)}o=o||u}if(o)return o!==s[0]&&s.unshift(o),n[o]}(p,F,i)),!c&&x.inArray("script",p.dataTypes)>-1&&x.inArray("json",p.dataTypes)<0&&(p.converters["text script"]=function(){}),w=function(e,t,n,r){var i,o,u,a,s,c={},l=e.dataTypes.slice();if(l[1])for(u in e.converters)c[u.toLowerCase()]=e.converters[u];for(o=l.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!s&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),s=o,o=l.shift())if("*"===o)o=s;else if("*"!==s&&s!==o){if(!(u=c[s+" "+o]||c["* "+o]))for(i in c)if((a=i.split(" "))[1]===o&&(u=c[s+" "+a[0]]||c["* "+a[0]])){!0===u?u=c[i]:!0!==c[i]&&(o=a[0],l.unshift(a[1]));break}if(!0!==u)if(u&&e.throws)t=u(t);else try{t=u(t)}catch(e){return{state:"parsererror",error:u?e:"No conversion from "+s+" to "+o}}}return{state:"success",data:t}}(p,w,F,c),c?(p.ifModified&&((D=F.getResponseHeader("Last-Modified"))&&(x.lastModified[o]=D),(D=F.getResponseHeader("etag"))&&(x.etag[o]=D)),204===e||"HEAD"===p.type?A="nocontent":304===e?A="notmodified":(A=w.state,h=w.data,c=!(d=w.error))):(d=A,!e&&A||(A="error",e<0&&(e=0))),F.status=e,F.statusText=(t||A)+"",c?b.resolveWith(y,[h,A,F]):b.rejectWith(y,[F,A,d]),F.statusCode(m),m=void 0,f&&v.trigger(c?"ajaxSuccess":"ajaxError",[F,p,c?h:d]),g.fireWith(y,[F,A]),f&&(v.trigger("ajaxComplete",[F,p]),--x.active||x.event.trigger("ajaxStop")))}return F},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,t){return x.get(e,void 0,t,"script")}}),x.each(["get","post"],(function(e,t){x[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),x.ajax(x.extend({url:e,type:t,dataType:i,data:n,success:r},x.isPlainObject(e)&&e))}})),x.ajaxPrefilter((function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")})),x._evalUrl=function(e,t,n){return x.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){x.globalEval(e,t,n)}})},x.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map((function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e})).append(this)),this},wrapInner:function(e){return g(e)?this.each((function(t){x(this).wrapInner(e.call(this,t))})):this.each((function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)}))},wrap:function(e){var t=g(e);return this.each((function(n){x(this).wrapAll(t?e.call(this,n):e)}))},unwrap:function(e){return this.parent(e).not("body").each((function(){x(this).replaceWith(this.childNodes)})),this}}),x.expr.pseudos.hidden=function(e){return!x.expr.pseudos.visible(e)},x.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},x.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(e){}};var Zt={0:200,1223:204},en=x.ajaxSettings.xhr();b.cors=!!en&&"withCredentials"in en,b.ajax=en=!!en,x.ajaxTransport((function(e){var t,r;if(b.cors||en&&!e.crossDomain)return{send:function(i,o){var u,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(u in e.xhrFields)a[u]=e.xhrFields[u];for(u in e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),e.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest"),i)a.setRequestHeader(u,i[u]);t=function(e){return function(){t&&(t=r=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===e?a.abort():"error"===e?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(Zt[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=t(),r=a.onerror=a.ontimeout=t("error"),void 0!==a.onabort?a.onabort=r:a.onreadystatechange=function(){4===a.readyState&&n.setTimeout((function(){t&&r()}))},t=t("abort");try{a.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}})),x.ajaxPrefilter((function(e){e.crossDomain&&(e.contents.script=!1)})),x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",(function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")})),x.ajaxTransport("script",(function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(r,i){t=x(" -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/attack-theme/templates/general/base-template.html b/attack-theme/templates/general/base-template.html index c960c51d76b..ab8e06538de 100644 --- a/attack-theme/templates/general/base-template.html +++ b/attack-theme/templates/general/base-template.html @@ -30,8 +30,10 @@ + {{ title }} + diff --git a/attack-theme/templates/macros/navigation_menu.html b/attack-theme/templates/macros/navigation_menu.html index c842b4d1027..64756461955 100644 --- a/attack-theme/templates/macros/navigation_menu.html +++ b/attack-theme/templates/macros/navigation_menu.html @@ -1,10 +1,12 @@ {% macro navigation_menu(menu, logo_header, output_file) -%}