docs: self-host fonts, images, and OpenAPI icons; block build egress (#39092)
* docs: self-host fonts, images, and OpenAPI icons; block build egress Stop hotlinking GitHub/user-images and Google Fonts on the docs site, replace embedded video iframes with links, and fail CI when a build would fetch off-origin resources (closed allowlist for the security-updates feed). Co-authored-by: Cursor <cursoragent@cursor.com> * docs: address CodeRabbit egress review findings Scan an offline (no-Algolia) rebuild in Docs CI, tighten ATTR/getAttr boundaries, flag remote template-literal prefixes before ${, and add alt text for the self-hosted plugin-management image. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: replace egress fixture tree with in-memory unit tests Keep scanner coverage without committing dozens of tiny HTML/CSS/JS fixture files. scanSources takes string maps; CLI smoke uses a temp dir. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: keep YouTube/Wistia embeds on hosted site Restore in-page players for the public docs. Allowlist those hosts in check-egress; the offline packager strips them for airgap. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: simplify hosted-embed allowlist to base domains Co-authored-by: Cursor <cursoragent@cursor.com> * docs: tighten hosted-embed allowlist to player kinds and paths Only iframe/script/img player URLs pass; fetch() and other loads to YouTube/Wistia hosts still fail egress. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: allow Wistia swatch preload in hosted-embed egress check The organize-conversations embed emits link[rel=preload] for the swatch; kind-scoped allowlisting missed it and failed Docs CI. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: use getAttr boundary for script hasSrc check \b matches inside data-src, so the old hasSrc could skip scanning an inline script body. Align with ATTR/getAttr and cover the case in tests. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
No files matched your search
@@ -48,9 +48,25 @@ jobs:
|
||||
working-directory: docs/site
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Egress-check unit tests
|
||||
working-directory: docs/site
|
||||
run: npm run test:egress
|
||||
|
||||
- name: Build docs site (includes OpenAPI prebuild)
|
||||
working-directory: docs/site
|
||||
run: npm run build
|
||||
env:
|
||||
ALGOLIA_APP_ID: ${{ vars.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_SEARCH_API_KEY: ${{ vars.ALGOLIA_SEARCH_API_KEY }}
|
||||
|
||||
# Hosted Algolia injects SearchBar preconnects; do not scan that artifact.
|
||||
# Rebuild without ALGOLIA_* (prebuild already ran) for the offline shape.
|
||||
- name: Rebuild without Algolia for egress scan
|
||||
working-directory: docs/site
|
||||
run: npx docusaurus build && npm run postbuild
|
||||
env:
|
||||
BASE_URL: /
|
||||
|
||||
- name: Scan built site for egress
|
||||
working-directory: docs/site
|
||||
run: npm run check:egress
|
||||
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 393 KiB |
@@ -11,10 +11,10 @@ sidebar_position: 6
|
||||
1. Create a new workspace for a ticket/issue that you've claimed by going to the [mattermost-gitpod-config](https://github.com/mattermost/mattermost-gitpod-config/tree/master) repository and clicking the "Open in Gitpod" badge.
|
||||
|
||||
* You can also use the [Gitpod browser extension](https://www.gitpod.io/docs/configure/user-settings/browser-extension) to open up a repository in Gitpod, or manually prefix GitHub repository URLs with `gitpod.io/#`. What the extension does is add a green Gitpod button to repository pages on GitHub, and clicking it spins up a new environment for the repository on Gitpod.
|
||||

|
||||

|
||||
|
||||
2. You may need to sign in (through GitHub) to access the workspace on Gitpod. Once Gitpod has done loading, the user interface presented is that of VSCode.
|
||||

|
||||

|
||||
|
||||
#### :pencil2: Working on an issue/ticket
|
||||
|
||||
@@ -27,44 +27,44 @@ sidebar_position: 6
|
||||
5. Click the "Source Control" icon on the left sidebar. On the panel next to the side bar, you will see a list of the repositories in the workspace, and under each repository a list of the files that have changed (if any). Next to the label for `mattermost-webapp`, click the button with the branch name and the source control icon.
|
||||
|
||||
6. A dropdown will appear via the command palette. Click the option that says: "+ Create new branch..."
|
||||

|
||||

|
||||
|
||||
7. In the box that appears, name your new branch. A good name idea is to name it after the code that refers to your issue - in this case, this is the "Test Key", so a suitable name for the branch is `MM-T642`. Then, hit `Enter` on your keyboard.
|
||||

|
||||

|
||||
|
||||
8. Now that the branch has changed, you can commit your changes to it. Back in the source control panel, you can make a commit message in the text box above the "commit button" in the section for the `mattermost-webapp` repository. Then, press the "commit button". A modal may appear asking you to first stage your changes.
|
||||

|
||||

|
||||
|
||||
9. You can now publish your branch to the remote. However, as you will not have access to the main repository itself, you will be prompted to first create a fork. Click on the "publish branch button" on the source control panel. A popup will appear, asking if you would like to make a fork and push to that instead. On the popup, select the "create fork" button.
|
||||

|
||||

|
||||
|
||||
10. During the process of fork creation, you may be prompted to grant GitHub access to Gitpod's GitHub extension (you should allow this).
|
||||

|
||||

|
||||
|
||||
11. You may also be asked to update the permissions you give Gitpod to access GitHub through a popup. If this happens, you will have to restart the fork creation process (publish branch -> say yes to creating a fork). Click the "open access control" button on the popup.
|
||||

|
||||

|
||||
|
||||
12. You will get taken to the Integration section of Gitpod's settings. In the list of Git Providers on the page, find GitHub, click the three dots next to the listing, and select "Edit Permissions" from the dropdown menu.
|
||||

|
||||

|
||||
|
||||
13. A popup will appear with a list of permission checkboxes. It's a good idea to check all of them off so you don't need to go through this process again. When you're done checking off the permissions, click the "Update Permissions" button.
|
||||

|
||||

|
||||
|
||||
14. Another tab might also appear where on GitHub's end you accept the additional permissions Gitpod is requesting. Click on the "authorize gitpod-io" button.
|
||||

|
||||

|
||||
|
||||
15. Once the forking process is done, there will be a couple of popups that appear on Gitpod: one asking if you'd like to periodically run `git fetch` (which will periodically download content from the remote), another one asking if you'd like to create a pull request for the branch you're on right inside Gitpod/the VSCode editor, and finally one informing that the fork was successfully created. On the successful fork creation popup, click the `open on GitHub` option.
|
||||

|
||||

|
||||
|
||||
#### :mag: Creating a Pull Request (PR)
|
||||
|
||||
16. You'll get taken to GitHub, to the fork of the repository you've worked on in your account instead of the main one in the Mattermost organization. Navigate to the branch you've made on your fork if you're not there already.
|
||||
|
||||
* Near the top of the page will be a bar mentioning how many commits ahead/behind your branch is from the `master`/`main` branch of the main repository. There will also be two buttons: "contribute" and "sync fork". Click on the "contribute" button, and in the dropdown that appears, click the "open pull request" button.
|
||||

|
||||

|
||||
|
||||
17. You'll be taken to another page on GitHub called "Open a pull request".
|
||||

|
||||

|
||||
|
||||
```text
|
||||
* The first section of the page compares whether the branches (your branch on your fork - the "head repository" vs. the master/main branch on the main repository - the "base repository") can be merged automatically.
|
||||
@@ -95,7 +95,7 @@ sidebar_position: 6
|
||||
```
|
||||
|
||||
18. Once you've created your pull request, you'll get taken to its page, like this one: [MM-T642: Attachment does not collapse - Cypress Webapp E2E Test #11231](https://github.com/mattermost/mattermost/pull/11231). Below your initial body text of the PR will be a list of commits and other comments. At the end of this list is a checklist which notes the status of reviews required for the pull request, and the checks that the pull request must pass, plus a place to write your own additional comments.
|
||||

|
||||

|
||||
|
||||
19. if you need to make any changes to your PR, you can return to Gitpod and stage and commit your changes from there onto your branch; and this will be reflected in GitHub.
|
||||
|
||||
|
||||
@@ -146,9 +146,9 @@ This starts a headless `delve` process for your IDE to connect to. The process l
|
||||
|
||||
Run the debugger in VSCode by navigating to the "Run and Debug" tab on the left side of the IDE, selecting your launch configuration, and clicking the green play button. This should bring up a debugging widget that looks like this:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Your IDE's debugger is now running and ready to pause your plugin's execution at any breakpoints you set in the IDE.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -66,7 +66,7 @@ In v5.16 and v5.17 the Marketplace only supports the installation of new plugins
|
||||
|
||||
There are two settings in **System Console > Plugin Management**:
|
||||
|
||||

|
||||

|
||||
|
||||
- **Enable Marketplace:** Turns the Marketplace user interface on or off for System Admins (end users cannot see the Marketplace).
|
||||
- **Marketplace URL:** The location of the Marketplace server to query for new plugins. Mattermost hosts a Marketplace for the community and this is the default value for this field. You can also set up your own Marketplace server.
|
||||
|
||||
|
After Width: | Height: | Size: 82 KiB |
@@ -120,7 +120,7 @@ Content-Type: application/json
|
||||
|
||||
When there is a `props` object with a `card` property attached to the webhook payload, the posted message displays a small info icon next to the timestamp. Clicking this icon expands the right-hand side panel to display the Markdown included in the `card` property:
|
||||
|
||||

|
||||

|
||||
|
||||
### Slack compatibility
|
||||
|
||||
|
||||
@@ -74,19 +74,7 @@ const config: Config = {
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
// Google Fonts: Inter (body) + Archivo Black (headings, free Trade Gothic Heavy fallback).
|
||||
// Replace with Adobe Fonts kit once Trade Gothic Next licensing is confirmed
|
||||
// — see PLAN.md §12 open items.
|
||||
stylesheets: [
|
||||
{
|
||||
href: 'https://fonts.googleapis.com/css2?family=Archivo+Black&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap',
|
||||
type: 'text/css',
|
||||
},
|
||||
],
|
||||
headTags: [
|
||||
{tagName: 'link', attributes: {rel: 'preconnect', href: 'https://fonts.googleapis.com'}},
|
||||
{tagName: 'link', attributes: {rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous'}},
|
||||
],
|
||||
// Fonts are self-hosted from src/css/fonts (see the README there).
|
||||
|
||||
// Use the classic preset for the default Documentation instance.
|
||||
// Developers and API are added as separate plugin instances below
|
||||
@@ -104,7 +92,7 @@ const config: Config = {
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
customCss: ['./src/css/tokens.css', './src/css/custom.css'],
|
||||
customCss: ['./src/css/fonts.css', './src/css/tokens.css', './src/css/custom.css'],
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"postbuild": "node scripts/duplicate-redirect-stubs.mjs",
|
||||
"postbuild": "node scripts/duplicate-redirect-stubs.mjs && node scripts/self-host-openapi-tab-icons.mjs",
|
||||
"stage:agents-docs": "node scripts/stage-agents-docs.mjs",
|
||||
"build:sidebars": "node scripts/gen-documentation-sidebar.mjs && node scripts/gen-developer-sidebar.mjs",
|
||||
"build:openapi:spec": "node scripts/build-openapi.mjs",
|
||||
@@ -17,6 +17,8 @@
|
||||
"build:plugin-manifest-docs": "mkdir -p data && go run -C scripts/gen-plugin-manifest-docs . > data/plugin-manifest-docs.json.tmp && mv data/plugin-manifest-docs.json.tmp data/plugin-manifest-docs.json",
|
||||
"build:plugin-docs": "npm run build:plugin-godocs && npm run build:plugin-jsdocs && npm run build:plugin-manifest-docs",
|
||||
"check:base-url-attrs": "node scripts/check-base-url-attrs.mjs",
|
||||
"check:egress": "node scripts/check-egress.mjs",
|
||||
"test:egress": "node --test scripts/check-egress.test.mjs",
|
||||
"prestart": "npm run stage:agents-docs && npm run build:sidebars && ( [ -f openapi/mattermost-openapi-v4.yaml ] || npm run build:openapi:spec ) && npm run build:openapi:docs && npm run build:plugin-docs",
|
||||
"prebuild": "npm run check:base-url-attrs && npm run stage:agents-docs && npm run build:sidebars && npm run build:openapi && npm run build:plugin-docs",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
#!/usr/bin/env node
|
||||
// Fail the build when HTML/CSS/JS would fetch an off-origin resource.
|
||||
// Closed allowlist: security-updates feed + docs.mattermost.com meta/canonical.
|
||||
|
||||
import {readdirSync, readFileSync, statSync} from 'node:fs';
|
||||
import {dirname, join, relative, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_ROOT = resolve(HERE, '..', 'build');
|
||||
|
||||
export const FEED_URL =
|
||||
'https://securityupdates.mattermost.com/security_updates.json';
|
||||
export const DOCS_HOST = 'docs.mattermost.com';
|
||||
export const EMPTY_BUILD_MESSAGE =
|
||||
'docs/site/build is missing or empty; run npm run build first';
|
||||
|
||||
// Hosted docs keep YouTube/Wistia players; build-offline-bundle.py strips them
|
||||
// from the airgap tarball. Allowlisted so check-egress matches the public site.
|
||||
const HOSTED_EMBED_BASES = [
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'wistia.com',
|
||||
'wistia.net',
|
||||
];
|
||||
|
||||
const TEXT_EXT = new Set(['.html', '.css', '.js']);
|
||||
// Attribute-name boundary: \b matches after - and :, so data-src / xlink:href
|
||||
// must not count as src / href. Require whitespace, quote, or / before the name.
|
||||
const ATTR =
|
||||
/(?<=[\s"'\/])(src|href|poster|data|srcset|content)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
||||
const CSS_URL =
|
||||
/(?:@import\s+(?:url\s*\()?['"]?(?:https?:)?\/\/[^'")\s]+)|(?:url\(\s*['"]?(?:https?:)?\/\/[^'")\s]+)/gi;
|
||||
// Quoted URL, complete template literal, or template with a remote prefix
|
||||
// before ${ (e.g. fetch(`https://api.example/${path}`)).
|
||||
const JS_FETCH =
|
||||
/\b(?:fetch|import)\s*\(\s*(?:['"]((?:https?:)?\/\/[^'"]+)['"]|`((?:https?:)?\/\/(?:(?!\$\{)[^`])+)`|`((?:https?:)?\/\/[^`$]*)\$\{)/g;
|
||||
const STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
const SCRIPT_BLOCK = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
||||
|
||||
const LOAD_RELS = new Set([
|
||||
'stylesheet',
|
||||
'preload',
|
||||
'modulepreload',
|
||||
'preconnect',
|
||||
'dns-prefetch',
|
||||
'prefetch',
|
||||
'prerender',
|
||||
'icon',
|
||||
'apple-touch-icon',
|
||||
'mask-icon',
|
||||
'manifest',
|
||||
]);
|
||||
|
||||
const JSON_SCRIPT_TYPES = new Set([
|
||||
'application/json',
|
||||
'application/ld+json',
|
||||
]);
|
||||
|
||||
export function isRemoteUrl(raw) {
|
||||
const t = String(raw ?? '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
t.startsWith('#') ||
|
||||
t.startsWith('data:') ||
|
||||
t.startsWith('blob:') ||
|
||||
t.startsWith('mailto:') ||
|
||||
t.startsWith('tel:')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return t.startsWith('https://') || t.startsWith('http://') || t.startsWith('//');
|
||||
}
|
||||
|
||||
export function reportUrl(raw) {
|
||||
const t = String(raw).trim();
|
||||
return t.startsWith('//') ? `https:${t}` : t;
|
||||
}
|
||||
|
||||
function isAllowedFeed(url) {
|
||||
return reportUrl(url) === FEED_URL;
|
||||
}
|
||||
|
||||
function isAllowedDocsMeta(url) {
|
||||
const reported = reportUrl(url);
|
||||
if (!reported.startsWith('https://')) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new URL(reported).hostname === DOCS_HOST;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isHostedEmbedHost(host) {
|
||||
return HOSTED_EMBED_BASES.some(
|
||||
(base) => host === base || host.endsWith('.' + base),
|
||||
);
|
||||
}
|
||||
|
||||
function isYoutubeHost(host) {
|
||||
return host === 'youtube.com' || host.endsWith('.youtube.com') || host === 'youtu.be' || host.endsWith('.youtu.be');
|
||||
}
|
||||
|
||||
function isWistiaHost(host) {
|
||||
return (
|
||||
host === 'wistia.com' ||
|
||||
host.endsWith('.wistia.com') ||
|
||||
host === 'wistia.net' ||
|
||||
host.endsWith('.wistia.net')
|
||||
);
|
||||
}
|
||||
|
||||
/** Allow only player tags/paths used by hosted docs embeds — not arbitrary host traffic. */
|
||||
export function isAllowedHostedEmbed(url, kind) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(reportUrl(url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (!isHostedEmbedHost(host)) {
|
||||
return false;
|
||||
}
|
||||
const path = parsed.pathname;
|
||||
|
||||
if (kind === 'iframe[src]') {
|
||||
if (isYoutubeHost(host) && path.startsWith('/embed/')) {
|
||||
return true;
|
||||
}
|
||||
if (isWistiaHost(host) && (path.startsWith('/embed/') || path.startsWith('/medias/'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (kind === 'script[src]') {
|
||||
if (!isWistiaHost(host)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
path.startsWith('/assets/external/') ||
|
||||
(path.startsWith('/embed/medias/') && path.endsWith('.jsonp'))
|
||||
);
|
||||
}
|
||||
// Wistia MDX embed also emits <link rel=preload> for the swatch image.
|
||||
if (kind === 'img[src]' || kind === 'link[rel=preload]') {
|
||||
return isWistiaHost(host) && path.startsWith('/embed/medias/');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function lineAt(text, index) {
|
||||
return text.slice(0, index).split('\n').length;
|
||||
}
|
||||
|
||||
function attrValue(match) {
|
||||
return match[2] ?? match[3] ?? match[4] ?? '';
|
||||
}
|
||||
|
||||
function getAttr(tagOpen, name) {
|
||||
const re = new RegExp(
|
||||
`(?<=[\\s"'\\/])${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`,
|
||||
'i',
|
||||
);
|
||||
const m = tagOpen.match(re);
|
||||
return m ? (m[1] ?? m[2] ?? m[3]) : null;
|
||||
}
|
||||
|
||||
function enclosingTag(html, attrIndex) {
|
||||
const start = html.lastIndexOf('<', attrIndex);
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
const gt = html.indexOf('>', attrIndex);
|
||||
const raw = gt >= 0 ? html.slice(start, gt + 1) : html.slice(start);
|
||||
const tagMatch = raw.match(/^<\s*([a-zA-Z][\w:-]*)/);
|
||||
if (!tagMatch) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
tag: tagMatch[1].toLowerCase(),
|
||||
rel: getAttr(raw, 'rel'),
|
||||
property: getAttr(raw, 'property'),
|
||||
name: getAttr(raw, 'name'),
|
||||
type: getAttr(raw, 'type'),
|
||||
};
|
||||
}
|
||||
|
||||
function srcsetUrls(value) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((part) => part.trim().split(/\s+/)[0])
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function relKind(relRaw) {
|
||||
const rels = (relRaw || '').toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||||
if (rels.includes('canonical')) {
|
||||
return 'canonical';
|
||||
}
|
||||
if (rels.includes('alternate')) {
|
||||
return 'skip';
|
||||
}
|
||||
if (rels.includes('shortcut') && rels.includes('icon')) {
|
||||
return 'link[rel=shortcut icon]';
|
||||
}
|
||||
const hit = rels.find((r) => LOAD_RELS.has(r));
|
||||
return hit ? `link[rel=${hit}]` : 'skip';
|
||||
}
|
||||
|
||||
function htmlKind(tag, attr, ctx) {
|
||||
const t = tag;
|
||||
const a = attr.toLowerCase();
|
||||
|
||||
if (t === 'a' || t === 'area') {
|
||||
return 'skip';
|
||||
}
|
||||
if (t === 'script' && a === 'src') {
|
||||
return 'script[src]';
|
||||
}
|
||||
if (t === 'link' && a === 'href') {
|
||||
return relKind(ctx.rel);
|
||||
}
|
||||
if (t === 'meta' && a === 'content') {
|
||||
const prop = (ctx.property || '').toLowerCase();
|
||||
const name = (ctx.name || '').toLowerCase();
|
||||
if (prop.startsWith('og:')) {
|
||||
return `meta[property=${prop}]`;
|
||||
}
|
||||
if (name.startsWith('twitter:')) {
|
||||
return `meta[name=${name}]`;
|
||||
}
|
||||
return 'skip';
|
||||
}
|
||||
if (t === 'img' && (a === 'src' || a === 'srcset')) {
|
||||
return `img[${a}]`;
|
||||
}
|
||||
if (t === 'source' && (a === 'src' || a === 'srcset')) {
|
||||
return `source[${a}]`;
|
||||
}
|
||||
if (t === 'video' && (a === 'src' || a === 'poster')) {
|
||||
return `video[${a}]`;
|
||||
}
|
||||
if (t === 'audio' && a === 'src') {
|
||||
return 'audio[src]';
|
||||
}
|
||||
if (t === 'track' && a === 'src') {
|
||||
return 'track[src]';
|
||||
}
|
||||
if (t === 'iframe' && a === 'src') {
|
||||
return 'iframe[src]';
|
||||
}
|
||||
if (t === 'embed' && a === 'src') {
|
||||
return 'embed[src]';
|
||||
}
|
||||
if (t === 'object' && a === 'data') {
|
||||
return 'object[data]';
|
||||
}
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
function pushViolation(violations, file, line, kind, found) {
|
||||
const url = reportUrl(found);
|
||||
if (isAllowedFeed(url) || isAllowedHostedEmbed(url, kind)) {
|
||||
return;
|
||||
}
|
||||
violations.push({file, line, kind, found: url});
|
||||
}
|
||||
|
||||
function considerUrl(violations, file, line, kind, raw) {
|
||||
if (kind === 'skip' || !isRemoteUrl(raw)) {
|
||||
return;
|
||||
}
|
||||
if (kind === 'canonical' || kind.startsWith('meta[property=og') || kind.startsWith('meta[name=twitter')) {
|
||||
if (isAllowedDocsMeta(raw)) {
|
||||
return;
|
||||
}
|
||||
const reportedKind = kind === 'canonical' ? 'link[rel=canonical]' : kind;
|
||||
pushViolation(violations, file, line, reportedKind, raw);
|
||||
return;
|
||||
}
|
||||
pushViolation(violations, file, line, kind, raw);
|
||||
}
|
||||
|
||||
function scanHtmlAttrs(html, file, violations) {
|
||||
ATTR.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = ATTR.exec(html))) {
|
||||
const ctx = enclosingTag(html, match.index);
|
||||
if (!ctx) {
|
||||
continue;
|
||||
}
|
||||
const attr = match[1];
|
||||
const kind = htmlKind(ctx.tag, attr, ctx);
|
||||
const value = attrValue(match);
|
||||
const line = lineAt(html, match.index);
|
||||
if (attr.toLowerCase() === 'srcset') {
|
||||
for (const candidate of srcsetUrls(value)) {
|
||||
considerUrl(violations, file, line, kind, candidate);
|
||||
}
|
||||
} else {
|
||||
considerUrl(violations, file, line, kind, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cssKind(matchText) {
|
||||
return /@import/i.test(matchText) ? 'css @import' : 'css url()';
|
||||
}
|
||||
|
||||
function urlFromCssMatch(text) {
|
||||
const found = text.match(/((?:https?:)?\/\/[^'")\s]+)/);
|
||||
return found ? found[1] : text;
|
||||
}
|
||||
|
||||
function scanCssIn(text, file, violations, fullFile, bodyStart) {
|
||||
CSS_URL.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = CSS_URL.exec(text))) {
|
||||
const raw = urlFromCssMatch(match[0]);
|
||||
if (!isRemoteUrl(raw)) {
|
||||
continue;
|
||||
}
|
||||
const index = (bodyStart ?? 0) + match.index;
|
||||
const source = fullFile ?? text;
|
||||
pushViolation(violations, file, lineAt(source, index), cssKind(match[0]), raw);
|
||||
}
|
||||
}
|
||||
|
||||
function scanJs(text, file, violations, fullFile, bodyStart) {
|
||||
JS_FETCH.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = JS_FETCH.exec(text))) {
|
||||
const raw = match[1] ?? match[2] ?? match[3];
|
||||
if (!isRemoteUrl(raw)) {
|
||||
continue;
|
||||
}
|
||||
const index = (bodyStart ?? 0) + match.index;
|
||||
const source = fullFile ?? text;
|
||||
const kind = match[0].trimStart().startsWith('import')
|
||||
? 'import()'
|
||||
: 'fetch()';
|
||||
considerUrl(violations, file, lineAt(source, index), kind, raw);
|
||||
}
|
||||
}
|
||||
|
||||
function isJsonScript(attrText) {
|
||||
const type = (getAttr(`<script ${attrText}>`, 'type') || '').toLowerCase();
|
||||
return JSON_SCRIPT_TYPES.has(type);
|
||||
}
|
||||
|
||||
function hasSrc(attrText) {
|
||||
// Same attribute boundary as getAttr/ATTR — \b matches inside data-src.
|
||||
return getAttr(`<script ${attrText}>`, 'src') != null;
|
||||
}
|
||||
|
||||
function scanHtml(html, file, violations) {
|
||||
scanHtmlAttrs(html, file, violations);
|
||||
|
||||
STYLE_BLOCK.lastIndex = 0;
|
||||
let style;
|
||||
while ((style = STYLE_BLOCK.exec(html))) {
|
||||
const bodyStart = style.index + style[0].indexOf(style[1]);
|
||||
scanCssIn(style[1], file, violations, html, bodyStart);
|
||||
}
|
||||
|
||||
SCRIPT_BLOCK.lastIndex = 0;
|
||||
let script;
|
||||
while ((script = SCRIPT_BLOCK.exec(html))) {
|
||||
if (hasSrc(script[1]) || isJsonScript(script[1])) {
|
||||
continue;
|
||||
}
|
||||
const bodyStart = script.index + script[0].indexOf(script[2]);
|
||||
scanJs(script[2], file, violations, html, bodyStart);
|
||||
}
|
||||
}
|
||||
|
||||
function* walk(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, {withFileTypes: true});
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
throw new Error(EMPTY_BUILD_MESSAGE);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules') {
|
||||
continue;
|
||||
}
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
yield* walk(full);
|
||||
} else if (entry.isFile()) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extOf(file) {
|
||||
const base = file.toLowerCase();
|
||||
if (base.endsWith('.html')) {
|
||||
return '.html';
|
||||
}
|
||||
if (base.endsWith('.css')) {
|
||||
return '.css';
|
||||
}
|
||||
if (base.endsWith('.js')) {
|
||||
return '.js';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Scan in-memory sources. Keys are relative paths; values are file text. */
|
||||
export function scanSources({html = {}, css = {}, js = {}} = {}) {
|
||||
const violations = [];
|
||||
const stats = {html: 0, css: 0, js: 0};
|
||||
|
||||
for (const [file, text] of Object.entries(html)) {
|
||||
stats.html++;
|
||||
scanHtml(text, file, violations);
|
||||
}
|
||||
for (const [file, text] of Object.entries(css)) {
|
||||
stats.css++;
|
||||
if (text.includes('://') || text.includes('url(//')) {
|
||||
scanCssIn(text, file, violations, text, 0);
|
||||
}
|
||||
}
|
||||
for (const [file, text] of Object.entries(js)) {
|
||||
stats.js++;
|
||||
if (text.includes('fetch(') || text.includes('import(')) {
|
||||
scanJs(text, file, violations, text, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.html === 0) {
|
||||
throw new Error(EMPTY_BUILD_MESSAGE);
|
||||
}
|
||||
|
||||
violations.stats = stats;
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function scanBuild(root) {
|
||||
const abs = resolve(root);
|
||||
try {
|
||||
if (!statSync(abs).isDirectory()) {
|
||||
throw new Error(EMPTY_BUILD_MESSAGE);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.message === EMPTY_BUILD_MESSAGE) {
|
||||
throw err;
|
||||
}
|
||||
if (err && err.code === 'ENOENT') {
|
||||
throw new Error(EMPTY_BUILD_MESSAGE);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const html = {};
|
||||
const css = {};
|
||||
const js = {};
|
||||
|
||||
for (const file of walk(abs)) {
|
||||
if (file.endsWith('.map')) {
|
||||
continue;
|
||||
}
|
||||
const ext = extOf(file);
|
||||
if (!TEXT_EXT.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
const rel = relative(abs, file).split('\\').join('/');
|
||||
const text = readFileSync(file, 'utf8');
|
||||
if (ext === '.html') {
|
||||
html[rel] = text;
|
||||
} else if (ext === '.css') {
|
||||
css[rel] = text;
|
||||
} else {
|
||||
js[rel] = text;
|
||||
}
|
||||
}
|
||||
|
||||
return scanSources({html, css, js});
|
||||
}
|
||||
|
||||
function fixHint(kind, found) {
|
||||
if (
|
||||
kind.includes('preconnect') ||
|
||||
found.includes('fonts.googleapis.com') ||
|
||||
found.includes('fonts.gstatic.com')
|
||||
) {
|
||||
return (
|
||||
'Fonts are already self-hosted in src/css/fonts.css; drop the tag\n' +
|
||||
' from docusaurus.config.ts stylesheets/headTags.'
|
||||
);
|
||||
}
|
||||
if (kind === 'fetch()' || kind === 'import()') {
|
||||
return (
|
||||
'Remove the runtime fetch or, if it is the security bulletin, use\n' +
|
||||
` exactly ${FEED_URL}`
|
||||
);
|
||||
}
|
||||
return 'Self-host the asset or remove the tag.';
|
||||
}
|
||||
|
||||
export function formatReport(rootLabel, violations) {
|
||||
const {html, css, js} = violations.stats;
|
||||
if (violations.length === 0) {
|
||||
return `[check-egress] ok: ${html} html, ${css} css, ${js} js, 0 external loads\n`;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`[check-egress] ${violations.length} external resource(s) in ${rootLabel} ` +
|
||||
`(scanned ${html} html, ${css} css, ${js} js).`,
|
||||
'',
|
||||
'A browser would fetch these. Self-host the asset or remove the tag.',
|
||||
'Do not add allowlist entries without a design review.',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const v of violations) {
|
||||
lines.push(` ${v.file}:${v.line}`);
|
||||
lines.push(` kind: ${v.kind}`);
|
||||
lines.push(` found: ${v.found}`);
|
||||
lines.push(` fix: ${fixHint(v.kind, v.found)}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('Allowlisted (closed):');
|
||||
lines.push(` - fetch ${FEED_URL}`);
|
||||
lines.push(
|
||||
` - <link rel=canonical> and og:*/twitter:* meta on https://${DOCS_HOST}`,
|
||||
);
|
||||
lines.push(' - <a href> / <area href> (never fetched; includes editUrl)');
|
||||
lines.push(
|
||||
' - YouTube/Wistia embeds (hosted site only; stripped in offline packaging)',
|
||||
);
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function main(argv) {
|
||||
const root = argv[0] ? resolve(argv[0]) : DEFAULT_ROOT;
|
||||
const label = argv[0] ? relative(process.cwd(), root) || root : 'docs/site/build';
|
||||
let violations;
|
||||
try {
|
||||
violations = scanBuild(root);
|
||||
} catch (err) {
|
||||
console.error(`[check-egress] ${err.message}`);
|
||||
return 1;
|
||||
}
|
||||
const report = formatReport(label, violations);
|
||||
if (violations.length > 0) {
|
||||
console.error(report.replace(/\n$/, ''));
|
||||
return 1;
|
||||
}
|
||||
process.stdout.write(report);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const isMain =
|
||||
process.argv[1] &&
|
||||
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
||||
if (isMain) {
|
||||
process.exit(main(process.argv.slice(2)));
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {mkdirSync, mkdtempSync, rmSync, writeFileSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {dirname, join} from 'node:path';
|
||||
import {after, describe, it} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
import {
|
||||
EMPTY_BUILD_MESSAGE,
|
||||
FEED_URL,
|
||||
isAllowedHostedEmbed,
|
||||
main,
|
||||
scanBuild,
|
||||
scanSources,
|
||||
} from './check-egress.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SCRIPT = join(HERE, 'check-egress.mjs');
|
||||
|
||||
function kinds(hits) {
|
||||
return hits.map((h) => h.kind);
|
||||
}
|
||||
|
||||
function found(hits) {
|
||||
return hits.map((h) => h.found);
|
||||
}
|
||||
|
||||
function stubHtml(extra = '') {
|
||||
return `<!DOCTYPE html><html><body>${extra}</body></html>`;
|
||||
}
|
||||
|
||||
describe('scanSources', () => {
|
||||
it('clean sources have 0 violations', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html': `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/_documentation/assets/css/x.css">
|
||||
<link rel="canonical" href="https://docs.mattermost.com/_documentation/">
|
||||
<meta property="og:image" content="https://docs.mattermost.com/_documentation/img/x.svg">
|
||||
<script src="/_documentation/assets/js/x.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<a href="https://github.com/mattermost/mattermost/tree/master/docs/main/foo.mdx">edit</a>
|
||||
<img src="/_documentation/images/x.png">
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
css: {'main.css': '@font-face { src: url(./fonts/inter.woff2); }'},
|
||||
js: {'app.js': `fetch("${FEED_URL}");`},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('flags script[src]', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': '<script src="https://cdn.example.com/foo.js"></script>'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'script[src]');
|
||||
assert.equal(hits[0].found, 'https://cdn.example.com/foo.js');
|
||||
});
|
||||
|
||||
it('flags link[rel=preconnect]', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'link[rel=preconnect]');
|
||||
assert.equal(hits[0].found, 'https://fonts.googleapis.com');
|
||||
});
|
||||
|
||||
it('flags link[rel=stylesheet]', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'link[rel=stylesheet]');
|
||||
});
|
||||
|
||||
it('flags SearchBar preconnect', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<link rel="preconnect" href="https://ABC-dsn.algolia.net" crossorigin>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'link[rel=preconnect]');
|
||||
assert.equal(hits[0].found, 'https://ABC-dsn.algolia.net');
|
||||
});
|
||||
|
||||
it('flags img[src]', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': '<img src="https://cdn.example.com/x.png">'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'img[src]');
|
||||
});
|
||||
|
||||
it('flags only the https srcset candidate', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<img srcset="https://cdn.example.com/a.png 1x, /_documentation/b.png 2x">',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'img[srcset]');
|
||||
assert.equal(hits[0].found, 'https://cdn.example.com/a.png');
|
||||
});
|
||||
|
||||
it('flags protocol-relative script[src] as https', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': '<script src="//cdn.example.com/x.js"></script>'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'script[src]');
|
||||
assert.equal(hits[0].found, 'https://cdn.example.com/x.js');
|
||||
});
|
||||
|
||||
it('flags css @import and url()', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
css: {
|
||||
'main.css':
|
||||
'@import url("https://fonts.googleapis.com/css");\n' +
|
||||
'body { background: url("https://fonts.gstatic.com/s/x.woff2"); }',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 2);
|
||||
assert.deepEqual(kinds(hits).sort(), ['css @import', 'css url()']);
|
||||
assert.ok(found(hits).includes('https://fonts.googleapis.com/css'));
|
||||
assert.ok(found(hits).includes('https://fonts.gstatic.com/s/x.woff2'));
|
||||
});
|
||||
|
||||
it('flags fetch()', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
js: {'app.js': 'fetch("https://evil.example/x");'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'fetch()');
|
||||
assert.equal(hits[0].found, 'https://evil.example/x');
|
||||
});
|
||||
|
||||
it('flags fetch() with a template literal', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
js: {'app.js': 'fetch(`https://evil.example/template`);'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'fetch()');
|
||||
assert.equal(hits[0].found, 'https://evil.example/template');
|
||||
});
|
||||
|
||||
it('flags remote prefix before ${', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
js: {'app.js': 'fetch(`https://api.example/${path}`);'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'fetch()');
|
||||
assert.equal(hits[0].found, 'https://api.example/');
|
||||
});
|
||||
|
||||
it('ignores data-src and xlink:href false positives', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html': `<img data-src="https://cdn.example.com/x.png" src="./local.png">
|
||||
<svg><use xlink:href="https://cdn.example.com/icons.svg#x"></use></svg>
|
||||
<link data-rel="stylesheet" href="./local.css">
|
||||
<script data-type="module" src="./ok.js"></script>`,
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('still scans inline script body when only data-src is set', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<script data-src="https://cdn.example.com/x.js">fetch("https://evil.example/x");</script>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'fetch()');
|
||||
});
|
||||
|
||||
it('allows the security bulletin URL', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
js: {'app.js': `fetch("${FEED_URL}");`},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('ignores <a href> and <area href>', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html': `<a href="https://forum.mattermost.com">forum</a>
|
||||
<a href="https://github.com/mattermost/mattermost">github</a>
|
||||
<a href="https://mattermost.com/pricing">pricing</a>
|
||||
<area href="https://example.com/map" alt="map">`,
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('does not parse application/json as JS', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<script type="application/json">{"x":"https://cdn.example.com/x.js"}</script>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('ignores SVG xmlns', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html': '<svg xmlns="http://www.w3.org/2000/svg"></svg>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('flags og:image off docs.mattermost.com', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<meta property="og:image" content="https://cdn.example.com/share.png">',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].found, 'https://cdn.example.com/share.png');
|
||||
});
|
||||
|
||||
it('throws when no HTML is present', () => {
|
||||
assert.throws(() => scanSources({css: {'a.css': 'body{}'}}), {
|
||||
message: EMPTY_BUILD_MESSAGE,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows YouTube iframes and Wistia scripts/images/preload', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html': `<iframe src="https://www.youtube.com/embed/abc"></iframe>
|
||||
<script src="https://fast.wistia.com/assets/external/E-v1.js"></script>
|
||||
<script src="https://fast.wistia.com/embed/medias/5gjgi10rr0.jsonp"></script>
|
||||
<img src="https://fast.wistia.com/embed/medias/x/swatch">
|
||||
<link rel="preload" href="https://fast.wistia.com/embed/medias/5gjgi10rr0/swatch" as="image">`,
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 0);
|
||||
});
|
||||
|
||||
it('rejects fetch() to an embed host', () => {
|
||||
const hits = scanSources({
|
||||
html: {'index.html': stubHtml()},
|
||||
js: {'app.js': 'fetch("https://www.youtube.com/embed/abc");'},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'fetch()');
|
||||
assert.equal(hits[0].found, 'https://www.youtube.com/embed/abc');
|
||||
});
|
||||
|
||||
it('rejects non-player paths on embed hosts', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<script src="https://www.youtube.com/yts/jsbin/player.js"></script>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
assert.equal(hits[0].kind, 'script[src]');
|
||||
});
|
||||
|
||||
it('still flags non-embed remote scripts', () => {
|
||||
const hits = scanSources({
|
||||
html: {
|
||||
'index.html':
|
||||
'<script src="https://cdn.example.com/foo.js"></script>',
|
||||
},
|
||||
});
|
||||
assert.equal(hits.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowedHostedEmbed', () => {
|
||||
it('allows player kinds/paths only', () => {
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed('https://www.youtube.com/embed/x', 'iframe[src]'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed('//fast.wistia.com/embed/medias/x', 'iframe[src]'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed(
|
||||
'https://fast.wistia.com/assets/external/E-v1.js',
|
||||
'script[src]',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed(
|
||||
'https://fast.wistia.com/embed/medias/x/swatch',
|
||||
'img[src]',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed(
|
||||
'https://fast.wistia.com/embed/medias/x/swatch',
|
||||
'link[rel=preload]',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed('https://www.youtube.com/embed/x', 'fetch()'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed('https://www.youtube.com/watch?v=x', 'iframe[src]'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedHostedEmbed('https://cdn.example.com/x', 'iframe[src]'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLI smoke (temp dir)', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'check-egress-'));
|
||||
const cleanDir = join(root, 'clean');
|
||||
const dirtyDir = join(root, 'dirty');
|
||||
const emptyDir = join(root, 'empty');
|
||||
|
||||
mkdirSync(cleanDir);
|
||||
mkdirSync(dirtyDir);
|
||||
mkdirSync(emptyDir);
|
||||
writeFileSync(
|
||||
join(cleanDir, 'index.html'),
|
||||
'<!DOCTYPE html><html><body><img src="/local.png"></body></html>',
|
||||
);
|
||||
writeFileSync(
|
||||
join(dirtyDir, 'index.html'),
|
||||
'<script src="https://cdn.example.com/foo.js"></script>',
|
||||
);
|
||||
|
||||
after(() => {
|
||||
rmSync(root, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
function runCli(dir) {
|
||||
return spawnSync(process.execPath, [SCRIPT, dir], {encoding: 'utf8'});
|
||||
}
|
||||
|
||||
it('scanBuild matches scanSources on a temp tree', () => {
|
||||
assert.equal(scanBuild(cleanDir).length, 0);
|
||||
assert.equal(scanBuild(dirtyDir).length, 1);
|
||||
});
|
||||
|
||||
it('exits 0 on a clean tree', () => {
|
||||
const result = runCli(cleanDir);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
|
||||
it('exits 1 when violations exist', () => {
|
||||
const result = runCli(dirtyDir);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /script\[src\]/);
|
||||
});
|
||||
|
||||
it('exits 1 on an empty tree', () => {
|
||||
const result = runCli(emptyDir);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /missing or empty/);
|
||||
});
|
||||
|
||||
it('main() returns 1 on violations', () => {
|
||||
assert.equal(main([dirtyDir]), 1);
|
||||
});
|
||||
|
||||
it('main() returns 0 on clean', () => {
|
||||
assert.equal(main([cleanDir]), 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
// Rewrite OpenAPI CodeTabs icon url()s from raw.githubusercontent.com
|
||||
// (devicons/devicon) to static/img/devicons.
|
||||
|
||||
import {existsSync, readdirSync, readFileSync, writeFileSync} from 'node:fs';
|
||||
import {dirname, join, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SITE = resolve(HERE, '..');
|
||||
const BUILD = join(SITE, 'build');
|
||||
const ICONS = join(SITE, 'static', 'img', 'devicons');
|
||||
const REMOTE =
|
||||
/url\(\s*['"]?https:\/\/raw\.githubusercontent\.com\/devicons\/devicon\/master\/icons\/[^/]+\/([^'")\s]+)['"]?\s*\)/g;
|
||||
|
||||
const icons = new Set(readdirSync(ICONS).filter((n) => n.endsWith('.svg')));
|
||||
const cssDir = join(BUILD, 'assets', 'css');
|
||||
if (!existsSync(cssDir)) {
|
||||
console.error(`[self-host-openapi-tab-icons] ${cssDir} not found; run \`docusaurus build\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
let rewritten = 0;
|
||||
let missing = [];
|
||||
|
||||
for (const name of readdirSync(cssDir)) {
|
||||
if (!name.endsWith('.css')) {
|
||||
continue;
|
||||
}
|
||||
const file = join(cssDir, name);
|
||||
const original = readFileSync(file, 'utf8');
|
||||
const next = original.replace(REMOTE, (match, basename) => {
|
||||
if (!icons.has(basename)) {
|
||||
missing.push(basename);
|
||||
return match;
|
||||
}
|
||||
rewritten++;
|
||||
return `url(../../img/devicons/${basename})`;
|
||||
});
|
||||
if (next !== original) {
|
||||
writeFileSync(file, next);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`[self-host-openapi-tab-icons] missing local icon(s): ${[...new Set(missing)].join(', ')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[self-host-openapi-tab-icons] rewrote ${rewritten} url() in assets/css`);
|
||||
@@ -146,7 +146,16 @@ export async function fetchSecurityUpdates(): Promise<{
|
||||
tabs: SecurityUpdateTab[];
|
||||
lastModified: string | null;
|
||||
}> {
|
||||
const response = await fetch(FEED_URL, {cache: 'no-store'});
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(FEED_URL, {cache: 'no-store'});
|
||||
} catch {
|
||||
// Prefer a clear offline/unreachable message over a raw "Failed to fetch".
|
||||
throw new Error(
|
||||
`This table is generated from a live feed at ${FEED_URL}, which could not be reached. ` +
|
||||
'If you are reading the documentation bundled with a Mattermost release, it requires internet access and is not available offline.',
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Security updates feed returned ${response.status}.`);
|
||||
}
|
||||
|
||||
@@ -126,10 +126,10 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Headlines target Trade Gothic Next Heavy proportions: tighter set
|
||||
* than Archivo Black's default. We tune letter-spacing per size and
|
||||
* apply a mild horizontal squeeze via font-stretch where the platform
|
||||
* supports it (no-op on Archivo Black, which is fixed-width).
|
||||
/* Headlines run a tighter set than Archivo Black's default. We tune
|
||||
* letter-spacing per size and apply a mild horizontal squeeze via
|
||||
* font-stretch where the platform supports it (no-op on Archivo Black,
|
||||
* which is fixed-width).
|
||||
*
|
||||
* h1 used to be 900-weight Archivo Black, but that read as too
|
||||
* heavy/shouty for page titles at body-copy reading distance — same
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* Self-hosted fonts from @fontsource; see fonts/README.md. Relative paths
|
||||
* so webpack prefixes them for the build baseUrl. latin/latin-ext only. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-ext-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-ext-500-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-500-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-ext-600-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-600-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-ext-700-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('./fonts/inter-latin-700-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo Black';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/archivo-black-latin-ext-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo Black';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/archivo-black-latin-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/jetbrains-mono-latin-ext-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./fonts/jetbrains-mono-latin-400-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('./fonts/jetbrains-mono-latin-ext-600-normal.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('./fonts/jetbrains-mono-latin-600-normal.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2017 The Archivo Black Project Authors (https://github.com/Omnibus-Type/ArchivoBlack)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Self-hosted web fonts
|
||||
|
||||
These replace the Google Fonts stylesheet the site used to load at runtime so
|
||||
the docs render without a network dependency on fonts.googleapis.com.
|
||||
|
||||
`@font-face` declarations live in `../fonts.css`, which is registered in
|
||||
`docusaurus.config.ts` under `presets[0][1].theme.customCss`. Paths there are
|
||||
relative, so webpack emits the files with the right prefix for whatever
|
||||
`baseUrl` the site is built against — do not move these into `static/`, where
|
||||
they would need an absolute `/fonts/...` URL and break the `/documentation/`
|
||||
build.
|
||||
|
||||
## Source
|
||||
|
||||
Extracted from the [Fontsource](https://fontsource.org) npm packages, which
|
||||
repackage the upstream Google Fonts releases:
|
||||
|
||||
| Family | Package | Version |
|
||||
| --- | --- | --- |
|
||||
| Inter | `@fontsource/inter` | 5.3.0 |
|
||||
| Archivo Black | `@fontsource/archivo-black` | 5.3.0 |
|
||||
| JetBrains Mono | `@fontsource/jetbrains-mono` | 5.3.0 |
|
||||
|
||||
These are not runtime dependencies — the `.woff2` files were taken from the
|
||||
published tarballs and committed directly, matching the approach in
|
||||
`webapp/channels/src/fonts/`. To refresh them, `npm pack` the package at the
|
||||
new version, copy the files listed below out of `package/files/`, and update the
|
||||
`unicode-range` values in `../fonts.css` from the matching `package/<weight>.css`.
|
||||
|
||||
## What is included
|
||||
|
||||
Only the subsets and weights the site actually uses, which is why this directory
|
||||
is around 360 KB rather than several megabytes:
|
||||
|
||||
| Family | Weights | Subsets |
|
||||
| --- | --- | --- |
|
||||
| Inter | 400, 500, 600, 700 | latin, latin-ext |
|
||||
| Archivo Black | 400 | latin, latin-ext |
|
||||
| JetBrains Mono | 400, 600 | latin, latin-ext |
|
||||
|
||||
Weights match what the old Google Fonts request asked for, so rendering is
|
||||
unchanged. No italic faces are shipped (none were requested before either);
|
||||
browsers synthesize them.
|
||||
|
||||
Only `.woff2` is included. Every browser the docs support reads it, and the
|
||||
`.woff` fallback would roughly double the payload for no practical gain.
|
||||
|
||||
## Licensing
|
||||
|
||||
All three families are licensed under the SIL Open Font License 1.1, which
|
||||
permits redistribution and bundling. The full license text for each is in this
|
||||
directory as `LICENSE-*.txt`.
|
||||
@@ -39,13 +39,10 @@
|
||||
--mm-marigold-700: #B27F00;
|
||||
|
||||
/* === Typography ===
|
||||
* Trade Gothic Next is licensed via Adobe Fonts and not embedded yet
|
||||
* (open item — see PLAN.md §12). Until licensing is confirmed, body uses
|
||||
* Inter and headlines use Archivo Black, which is the closest free match
|
||||
* for Trade Gothic Heavy's geometric, ultra-bold display feel. */
|
||||
--mm-font-sans: "Inter", "Trade Gothic Next", -apple-system, BlinkMacSystemFont,
|
||||
* Self-hosted from src/css/fonts (see the README there). */
|
||||
--mm-font-sans: "Inter", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Helvetica Neue", system-ui, sans-serif;
|
||||
--mm-font-heading: "Archivo Black", "Trade Gothic Next Heavy",
|
||||
--mm-font-heading: "Archivo Black",
|
||||
"Helvetica Neue", "Impact", system-ui, sans-serif;
|
||||
--mm-font-mono: "JetBrains Mono", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#293138" d="M112.205 26.129 71.8 2.142A15.326 15.326 0 0 0 64.005 0c-2.688 0-5.386.717-7.796 2.152L15.795 26.14C10.976 28.999 8 34.289 8 40.018v47.975c0 5.729 2.967 11.019 7.796 13.878L56.2 125.858A15.193 15.193 0 0 0 63.995 128a15.32 15.32 0 0 0 7.796-2.142l40.414-23.987c4.819-2.86 7.796-8.16 7.796-13.878V40.007c0-5.718-2.967-11.019-7.796-13.878zm-31.29 74.907.063 3.448c0 .418-.267.889-.588 1.06l-2.046 1.178c-.321.16-.6-.032-.6-.45l-.032-3.394c-1.745.728-3.523.9-4.647.45-.214-.086-.31-.397-.225-.761l.739-3.116c.064-.246.193-.493.364-.643a.725.725 0 0 1 .193-.139c.117-.064.235-.075.332-.032 1.22.407 2.773.214 4.272-.535 1.907-.964 3.18-2.913 3.16-4.84-.022-1.757-.964-2.474-3.267-2.496-2.934.01-5.675-.567-5.718-4.894-.032-3.555 1.81-7.26 4.744-9.595l-.032-3.48c0-.428.257-.9.589-1.07l1.98-1.264c.322-.161.6.043.6.46l.033 3.48c1.456-.578 2.72-.738 3.865-.47.247.064.364.406.257.802l-.77 3.084a1.372 1.372 0 0 1-.354.622.825.825 0 0 1-.203.15c-.108.053-.204.064-.3.053-.525-.118-1.767-.385-3.727.6-2.056 1.038-2.773 2.827-2.763 4.155.022 1.585.825 2.066 3.63 2.11 3.738.064 5.344 1.691 5.387 5.45.053 3.684-1.917 7.657-4.937 10.077zm21.18-5.794c0 .322-.042.621-.31.771l-10.216 6.211c-.267.161-.482.022-.482-.3V99.29c0-.321.193-.492.46-.653l10.067-6.018c.268-.16.482-.022.482.3zm7.026-58.993L70.89 59.86c-4.765 2.784-8.278 5.911-8.288 11.662v47.107c0 3.437 1.392 5.665 3.523 6.318a12.81 12.81 0 0 1-2.12.204c-2.239 0-4.445-.61-6.383-1.757L17.219 99.408c-3.951-2.345-6.403-6.725-6.403-11.426V40.007c0-4.7 2.452-9.08 6.403-11.426L57.634 4.594a12.555 12.555 0 0 1 6.382-1.756c2.238 0 4.444.61 6.382 1.756l40.415 23.987c3.33 1.981 5.579 5.397 6.21 9.242-1.36-2.86-4.38-3.63-7.902-1.574z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#a9bacd" d="M125 50c-4-32-24-50-62-50C29 0 3 24 3 64c0 39 24 64 64 64 32 0 55-19 58-50H87c-2 11-8 20-20 20-21 0-24-16-24-33 0-23 8-35 22-35 13 0 20 7 22 20z" /></svg>
|
||||
|
After Width: | Height: | Size: 241 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#9B4F96" d="M115.4 30.7L67.1 2.9c-.8-.5-1.9-.7-3.1-.7-1.2 0-2.3.3-3.1.7l-48 27.9c-1.7 1-2.9 3.5-2.9 5.4v55.7c0 1.1.2 2.4 1 3.5l106.8-62c-.6-1.2-1.5-2.1-2.4-2.7z"/><path fill="#68217A" d="M10.7 95.3c.5.8 1.2 1.5 1.9 1.9l48.2 27.9c.8.5 1.9.7 3.1.7 1.2 0 2.3-.3 3.1-.7l48-27.9c1.7-1 2.9-3.5 2.9-5.4V36.1c0-.9-.1-1.9-.6-2.8l-106.6 62z"/><path fill="#fff" d="M85.3 76.1C81.1 83.5 73.1 88.5 64 88.5c-13.5 0-24.5-11-24.5-24.5s11-24.5 24.5-24.5c9.1 0 17.1 5 21.3 12.5l13-7.5c-6.8-11.9-19.6-20-34.3-20-21.8 0-39.5 17.7-39.5 39.5s17.7 39.5 39.5 39.5c14.6 0 27.4-8 34.2-19.8l-12.9-7.6zM97 66.2l.9-4.3h-4.2v-4.7h5.1L100 51h4.9l-1.2 6.1h3.8l1.2-6.1h4.8l-1.2 6.1h2.4v4.7h-3.3l-.9 4.3h4.2v4.7h-5.1l-1.2 6h-4.9l1.2-6h-3.8l-1.2 6h-4.8l1.2-6h-2.4v-4.7H97zm4.8 0h3.8l.9-4.3h-3.8l-.9 4.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 851 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#00c4b3" d="M35.2 34.9l-8.3-8.3v59.7l.1 2.8c0 1.3.2 2.8.7 4.3l65.6 23.1 16.3-7.2-74.4-74.4z"/><path d="M27.7 93.4zm81.9 15.9l-16.3 7.2-65.4-23.1c1.3 4.8 4 10.1 7 13.2l21.3 21.2 47.6.1 5.8-18.6z" fill="#22d3c5"/><path fill="#0075c9" d="M1.7 65.1C-.4 67.3.7 72 4 75.5l14.7 14.8 9.2 3.3c-.3-1.5-.7-3-.7-4.3l-.1-2.8-.2-59.8m82.7 82.6l7.2-16.4-23-65.6c-1.5-.3-3-.6-4.3-.7l-2.9-.1-59.6.1"/><path d="M93.6 27.3c.2 0 .2 0 0 0 .2 0 .2 0 0 0zm16 82l17.7-5.8V54.8l-20.4-20.5c-3-3-8.3-5.8-13.2-7l23.1 65.6" fill="#00a8e1"/><path fill="#00c4b3" d="M90.5 18.2L75.7 3.5c-3.4-3.4-8-4.4-10.4-2.3L26.9 26.6h59.5l2.9.1c1.3 0 2.8.2 4.3.7l-3.1-9.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 710 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill="#00acd7" fill-rule="evenodd"><path d="M11.156 54.829c-.243 0-.303-.122-.182-.303l1.273-1.637c.12-.182.424-.303.666-.303H34.55c.243 0 .303.182.182.364l-1.03 1.576c-.121.181-.424.363-.606.363zM2.004 60.404c-.242 0-.303-.12-.182-.303l1.273-1.636c.121-.182.424-.303.667-.303h27.636c.242 0 .364.182.303.364l-.485 1.454c-.06.243-.303.364-.545.364zM16.67 65.98c-.242 0-.302-.182-.181-.364l.848-1.515c.122-.182.364-.363.607-.363h12.12c.243 0 .364.181.364.424l-.12 1.454c0 .243-.243.425-.425.425zM79.58 53.738c-3.819.97-6.425 1.697-10.182 2.666-.91.243-.97.303-1.758-.606-.909-1.03-1.576-1.697-2.848-2.303-3.819-1.878-7.516-1.333-10.97.91-4.121 2.666-6.242 6.605-6.182 11.514.06 4.849 3.394 8.849 8.182 9.516 4.121.545 7.576-.91 10.303-4 .545-.667 1.03-1.394 1.636-2.243H56.064c-1.272 0-1.575-.788-1.151-1.818.788-1.879 2.242-5.03 3.09-6.606.183-.364.607-.97 1.516-.97h22.06c-.12 1.637-.12 3.273-.363 4.91-.667 4.363-2.303 8.363-4.97 11.878-4.364 5.758-10.06 9.333-17.273 10.303-5.939.788-11.454-.364-16.302-4-4.485-3.394-7.03-7.879-7.697-13.454-.788-6.606 1.151-12.546 5.151-17.758 4.303-5.636 10-9.212 16.97-10.485 5.697-1.03 11.151-.363 16.06 2.97 3.212 2.121 5.515 5.03 7.03 8.545.364.546.122.849-.606 1.03z"/><path d="M99.64 87.253c-5.515-.122-10.546-1.697-14.788-5.334-3.576-3.09-5.818-7.03-6.545-11.697-1.091-6.848.787-12.909 4.909-18.302 4.424-5.819 9.757-8.849 16.97-10.122 6.181-1.09 12-.484 17.272 3.091 4.788 3.273 7.757 7.697 8.545 13.515 1.03 8.182-1.333 14.849-6.97 20.546-4 4.06-8.909 6.606-14.545 7.757-1.636.303-3.273.364-4.848.546zm14.424-24.485c-.06-.788-.06-1.394-.182-2-1.09-6-6.606-9.394-12.363-8.06-5.637 1.272-9.273 4.848-10.606 10.545-1.091 4.727 1.212 9.515 5.575 11.454 3.334 1.455 6.667 1.273 9.879-.363 4.788-2.485 7.394-6.364 7.697-11.576z" fill-rule="nonzero"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#0074BD" d="M47.617 98.12s-4.767 2.774 3.397 3.71c9.892 1.13 14.947.968 25.845-1.092 0 0 2.871 1.795 6.873 3.351-24.439 10.47-55.308-.607-36.115-5.969zm-2.988-13.665s-5.348 3.959 2.823 4.805c10.567 1.091 18.91 1.18 33.354-1.6 0 0 1.993 2.025 5.132 3.131-29.542 8.64-62.446.68-41.309-6.336z"/><path fill="#EA2D2E" d="M69.802 61.271c6.025 6.935-1.58 13.17-1.58 13.17s15.289-7.891 8.269-17.777c-6.559-9.215-11.587-13.792 15.635-29.58 0 .001-42.731 10.67-22.324 34.187z"/><path fill="#0074BD" d="M102.123 108.229s3.529 2.91-3.888 5.159c-14.102 4.272-58.706 5.56-71.094.171-4.451-1.938 3.899-4.625 6.526-5.192 2.739-.593 4.303-.485 4.303-.485-4.953-3.487-32.013 6.85-13.743 9.815 49.821 8.076 90.817-3.637 77.896-9.468zM49.912 70.294s-22.686 5.389-8.033 7.348c6.188.828 18.518.638 30.011-.326 9.39-.789 18.813-2.474 18.813-2.474s-3.308 1.419-5.704 3.053c-23.042 6.061-67.544 3.238-54.731-2.958 10.832-5.239 19.644-4.643 19.644-4.643zm40.697 22.747c23.421-12.167 12.591-23.86 5.032-22.285-1.848.385-2.677.72-2.677.72s.688-1.079 2-1.543c14.953-5.255 26.451 15.503-4.823 23.725 0-.002.359-.327.468-.617z"/><path fill="#EA2D2E" d="M76.491 1.587S89.459 14.563 64.188 34.51c-20.266 16.006-4.621 25.13-.007 35.559-11.831-10.673-20.509-20.07-14.688-28.815C58.041 28.42 81.722 22.195 76.491 1.587z"/><path fill="#0074BD" d="M52.214 126.021c22.476 1.437 57-.8 57.817-11.436 0 0-1.571 4.032-18.577 7.231-19.186 3.612-42.854 3.191-56.887.874 0 .001 2.875 2.381 17.647 3.331z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#F0DB4F" d="M1.408 1.408h125.184v125.185H1.408z"/><path fill="#323330" d="M116.347 96.736c-.917-5.711-4.641-10.508-15.672-14.981-3.832-1.761-8.104-3.022-9.377-5.926-.452-1.69-.512-2.642-.226-3.665.821-3.32 4.784-4.355 7.925-3.403 2.023.678 3.938 2.237 5.093 4.724 5.402-3.498 5.391-3.475 9.163-5.879-1.381-2.141-2.118-3.129-3.022-4.045-3.249-3.629-7.676-5.498-14.756-5.355l-3.688.477c-3.534.893-6.902 2.748-8.877 5.235-5.926 6.724-4.236 18.492 2.975 23.335 7.104 5.332 17.54 6.545 18.873 11.531 1.297 6.104-4.486 8.08-10.234 7.378-4.236-.881-6.592-3.034-9.139-6.949-4.688 2.713-4.688 2.713-9.508 5.485 1.143 2.499 2.344 3.63 4.26 5.795 9.068 9.198 31.76 8.746 35.83-5.176.165-.478 1.261-3.666.38-8.581zM69.462 58.943H57.753l-.048 30.272c0 6.438.333 12.34-.714 14.149-1.713 3.558-6.152 3.117-8.175 2.427-2.059-1.012-3.106-2.451-4.319-4.485-.333-.584-.583-1.036-.667-1.071l-9.52 5.83c1.583 3.249 3.915 6.069 6.902 7.901 4.462 2.678 10.459 3.499 16.731 2.059 4.082-1.189 7.604-3.652 9.448-7.401 2.666-4.915 2.094-10.864 2.07-17.444.06-10.735.001-21.468.001-32.237z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="500.003" x2="-.097" y1="579.106" y2="1079.206" gradientTransform="translate(15.534 -96.774) scale(.1939)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".003" stop-color="#e44857"/>
|
||||
<stop offset=".469" stop-color="#c711e1"/>
|
||||
<stop offset="1" stop-color="#7f52ff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#a)" d="M112.484 112.484H15.516V15.516h96.968L64 64Zm0 0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="url(#a)" d="M66.958.825a6.07 6.07 0 0 0-6.035 0L11.103 29.76c-1.895 1.072-2.96 3.095-2.96 5.24v57.988c0 2.143 1.183 4.167 2.958 5.24l49.82 28.934a6.07 6.07 0 0 0 6.036 0l49.82-28.935c1.894-1.072 2.958-3.096 2.958-5.24V35c0-2.144-1.183-4.167-2.958-5.24z"/><path fill="url(#b)" d="M116.897 29.76 66.841.825A8.161 8.161 0 0 0 65.302.23L9.21 96.798a6.251 6.251 0 0 0 1.657 1.43l50.057 28.934c1.42.833 3.076 1.072 4.615.595l52.66-96.925a3.702 3.702 0 0 0-1.302-1.072z"/><path fill="url(#c)" d="M116.898 98.225c1.42-.833 2.485-2.262 2.958-3.81L65.066.108c-1.42-.238-2.959-.119-4.26.715L11.104 29.639l53.606 98.355c.71-.12 1.54-.358 2.25-.715z"/><defs><linearGradient id="a" x1="34.513" x2="27.157" y1="15.535" y2="30.448" gradientTransform="translate(-129.242 -73.715) scale(6.18523)" gradientUnits="userSpaceOnUse"><stop stop-color="#3F873F"/><stop offset=".33" stop-color="#3F8B3D"/><stop offset=".637" stop-color="#3E9638"/><stop offset=".934" stop-color="#3DA92E"/><stop offset="1" stop-color="#3DAE2B"/></linearGradient><linearGradient id="b" x1="30.009" x2="50.533" y1="23.359" y2="8.288" gradientTransform="translate(-129.242 -73.715) scale(6.18523)" gradientUnits="userSpaceOnUse"><stop offset=".138" stop-color="#3F873F"/><stop offset=".402" stop-color="#52A044"/><stop offset=".713" stop-color="#64B749"/><stop offset=".908" stop-color="#6ABF4B"/></linearGradient><linearGradient id="c" x1="21.917" x2="40.555" y1="22.261" y2="22.261" gradientTransform="translate(-129.242 -73.715) scale(6.18523)" gradientUnits="userSpaceOnUse"><stop offset=".092" stop-color="#6ABF4B"/><stop offset=".287" stop-color="#64B749"/><stop offset=".598" stop-color="#52A044"/><stop offset=".862" stop-color="#3F873F"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill="#0b5a9d"><path d="M63.877 125.392c-32.671 0-60.37-27.594-60.627-60.469a59.94 59.94 0 0117.506-42.759 60.939 60.939 0 0143.279-18.36 60.081 60.081 0 0142.647 17.71 60.145 60.145 0 0118.157 42.522c.151 33.604-26.864 61.021-60.469 61.363h-.493zm.19-118.406a57.774 57.774 0 00-41.01 17.427 56.775 56.775 0 00-16.63 40.484c.236 31.159 26.495 57.286 57.43 57.286h.414c31.863-.29 57.504-26.266 57.385-58.128a56.97 56.97 0 00-17.217-40.273A56.7 56.7 0 0064.068 6.986z"/><path d="M16.89 82.383V46.865h8.64v3.183h-4.583v29.218h4.584v3.183l-8.642-.066zM46.213 64.272c0 6.478-3.933 10.167-9.26 10.167s-8.877-4.156-8.877-9.831c0-5.939 3.722-10.121 9.167-10.121s8.97 4.36 8.97 9.785zm-14.415.29c0 3.932 1.973 7.05 5.36 7.05s5.333-3.183 5.333-7.195c0-3.643-1.796-7.083-5.334-7.083s-5.392 3.328-5.392 7.307l.033-.08zM49.205 55.158c1.69-.29 3.407-.434 5.123-.428a9.17 9.17 0 015.537 1.223 4.062 4.062 0 012.006 3.61 4.48 4.48 0 01-3.183 4.183c2.269.46 3.9 2.46 3.9 4.775a5.016 5.016 0 01-1.861 3.978c-1.368 1.21-3.643 1.796-7.162 1.796a33.966 33.966 0 01-4.327-.257l-.033-18.88zm3.499 7.622h1.795c2.433 0 3.801-1.145 3.801-2.782 0-1.638-1.368-2.644-3.61-2.644a9.779 9.779 0 00-2.006.145l.02 5.28zm0 8.878c.618.065 1.243.092 1.86.078 2.263 0 4.262-.861 4.262-3.182s-1.94-3.183-4.373-3.183h-1.75v6.287zM69.54 54.901h3.517v12.554c0 5.334-2.577 7.116-6.365 7.116a9.313 9.313 0 01-2.973-.507l.428-2.834c.703.224 1.44.335 2.183.349 2.006 0 3.183-.921 3.183-4.262l.026-12.416zM83.067 65.357v2.434h-7.32v-2.434h7.32zM100.158 73.63c-1.585.632-3.281.921-4.978.862-6.129 0-9.851-3.834-9.851-9.707-.283-5.353 3.827-9.923 9.18-10.206.375-.02.757-.02 1.131.006a11.112 11.112 0 014.775.862l-.783 2.801a9.476 9.476 0 00-3.788-.75c-3.932 0-6.76 2.467-6.76 7.116 0 4.235 2.499 6.971 6.734 6.971a9.806 9.806 0 003.834-.717l.506 2.762zM111.2 46.766v35.61h-8.641v-3.182h4.583V49.949h-4.583v-3.183h8.64z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><linearGradient id="ocaml-original-a" gradientUnits="userSpaceOnUse" x1="82.925" y1="97.718" x2="82.925" y2="97.9" gradientTransform="translate(0 8.224) scale(.77317)"><stop offset="0" stop-color="#F29100"/><stop offset="1" stop-color="#EC670F"/></linearGradient><linearGradient id="ocaml-original-b" gradientUnits="userSpaceOnUse" x1="61.276" y1="98.981" x2="61.276" y2="144.277" gradientTransform="translate(0 8.224) scale(.77317)"><stop offset="0" stop-color="#F29100"/><stop offset="1" stop-color="#EC670F"/></linearGradient><linearGradient id="ocaml-original-c" gradientUnits="userSpaceOnUse" x1="82.781" y1="0" x2="82.781" y2="144.245" gradientTransform="translate(0 8.224) scale(.77317)"><stop offset="0" stop-color="#F29100"/><stop offset="1" stop-color="#EC670F"/></linearGradient><linearGradient id="ocaml-original-d" gradientUnits="userSpaceOnUse" x1="22.871" y1="92.114" x2="22.871" y2="143.249" gradientTransform="translate(0 8.224) scale(.77317)"><stop offset="0" stop-color="#F29100"/><stop offset="1" stop-color="#EC670F"/></linearGradient></defs><path d="M64.11 83.918l.019-.047c-.027-.117-.035-.148-.02.047zm0 0" fill="#484444"/><path d="M64.11 83.918l.019-.047c-.027-.117-.035-.148-.02.047zm0 0" fill="url(#ocaml-original-a)"/><path d="M64.969 115.441c-.45-.937-1.016-2.773-1.39-3.582-.356-.754-1.434-2.722-1.974-3.355-1.175-1.375-1.453-1.477-1.796-3.215-.602-3.023-2.192-8.508-4.067-12.293-.969-1.953-2.578-3.594-4.05-5.012-1.286-1.242-4.184-3.332-4.692-3.226-4.734.945-6.203 5.59-8.434 9.27-1.23 2.034-2.535 3.765-3.507 5.929-.899 1.992-.817 4.195-2.352 5.902-1.578 1.758-2.602 3.625-3.371 5.891-.148.43-.563 4.953-1.016 6.027l7.04-.496c6.558.45 4.663 2.965 14.902 2.414l16.164-.5c-.504-1.48-1.196-3.195-1.461-3.754zm0 0" fill="url(#ocaml-original-b)"/><path d="M111.875 8.223H16.133C7.227 8.227.008 15.445.008 24.352v35.183c2.308-.836 5.625-5.742 6.664-6.937 1.82-2.086 2.148-4.75 3.055-6.426 2.066-3.82 2.421-6.445 7.109-6.445 2.187 0 3.055.503 4.535 2.488 1.027 1.379 2.809 3.93 3.637 5.633.96 1.968 2.527 4.629 3.215 5.164.511.402 1.015.699 1.488.875.758.289 1.39-.235 1.898-.64.649-.52.93-1.571 1.532-2.977.867-2.028 1.808-4.458 2.347-5.31.93-1.464 1.246-3.202 2.25-4.046 1.485-1.242 3.414-1.328 3.95-1.434 2.976-.59 4.328 1.434 5.792 2.743.961.855 2.27 2.582 3.204 4.89.726 1.809 1.652 3.48 2.039 4.524.375 1.004 1.3 2.62 1.847 4.55.496 1.758 1.828 3.106 2.332 3.938 0 0 .774 2.168 5.48 4.152 1.02.43 3.083 1.13 4.313 1.575 2.047.746 4.028.648 6.551.347 1.797 0 2.774-2.605 3.59-4.691.484-1.23.945-4.766 1.262-5.766.304-.976-.41-1.73.199-2.586.71-1 1.133-1.054 1.543-2.355.883-2.785 5.984-2.926 8.847-2.926 2.399 0 2.086 2.32 6.141 1.527 2.32-.457 4.559.297 7.024.95 2.074.55 4.023 1.175 5.195 2.546.758.887 2.633 5.325.719 5.516.183.223.32.629.664.852-.426 1.675-2.282.48-3.309.265-1.39-.285-2.367.043-3.726.645-2.32 1.035-5.715.914-7.739 2.597-1.715 1.43-1.71 4.614-2.511 6.399 0 0-2.223 5.719-7.075 9.215-1.242.898-3.668 3.054-8.953 3.87-2.367.368-4.586.395-7.02.274l-3.511-.133c-.695-.003-3.063-.082-2.945.145l-.266.656c.043.219.129.754.152.887.098.527.125.95.145 1.437.035 1-.082 2.043-.031 3.055.105 2.094.882 4.008.98 6.121.11 2.356 1.274 4.844 2.399 6.77.43.73 1.078.812 1.363 1.715.332 1.03.02 2.129.18 3.23.632 4.266 1.855 8.73 3.785 12.582a.685.685 0 00.043.094c2.375-.395 4.754-1.25 7.84-1.707 5.652-.836 13.519-.406 18.57-.88 12.781-1.198 19.719 5.243 31.203 2.602V24.352c-.004-8.907-7.223-16.13-16.129-16.13zM64.109 83.918c-.015-.195-.007-.168.02-.047zm0 0" fill="url(#ocaml-original-c)"/><path d="M29.516 98.727c.89-1.946 1.406-4.165 2.144-6.157.711-1.906 1.817-4.61 3.7-5.57-.227-.27-3.938-.39-4.93-.492l-3.2-.453-6.164-1.27c-1.199-.289-5.187-1.703-6.054-2.101-2.032-.938-3.383-3.48-4.969-3.22-1.016.165-2.012.513-2.633 1.536-.515.836-.691 2.27-1.047 3.23-.414 1.118-1.129 2.16-1.754 3.223-1.152 1.95-3.222 3.715-4.113 5.617-.18.39-.34.828-.488 1.285v21.735l3.347.722c8.993 2.403 11.188 2.606 20.008 1.594l.828-.11c.672-1.405 1.196-6.187 1.633-7.667.34-1.137.809-2.04.988-3.2.168-1.1-.015-2.148-.113-3.152-.242-2.504 1.828-3.398 2.82-5.55zm0 0" fill="url(#ocaml-original-d)"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="url(#a)" d="M0 64c0 18.593 28.654 33.667 64 33.667 35.346 0 64-15.074 64-33.667 0-18.593-28.655-33.667-64-33.667C28.654 30.333 0 45.407 0 64Z"/><path fill="#777bb3" d="M64 95.167c33.965 0 61.5-13.955 61.5-31.167 0-17.214-27.535-31.167-61.5-31.167S2.5 46.786 2.5 64c0 17.212 27.535 31.167 61.5 31.167Z"/><path d="M34.772 67.864c2.793 0 4.877-.515 6.196-1.53 1.306-1.006 2.207-2.747 2.68-5.175.44-2.27.272-3.854-.5-4.71-.788-.874-2.493-1.317-5.067-1.317h-4.464l-2.473 12.732zM20.173 83.547a.694.694 0 0 1-.68-.828l6.557-33.738a.695.695 0 0 1 .68-.561h14.134c4.442 0 7.748 1.206 9.827 3.585 2.088 2.39 2.734 5.734 1.917 9.935-.333 1.711-.905 3.3-1.7 4.724a15.818 15.818 0 0 1-3.128 3.92c-1.531 1.432-3.264 2.472-5.147 3.083-1.852.604-4.232.91-7.07.91h-5.724l-1.634 8.408a.695.695 0 0 1-.682.562z"/><path fill="#fff" d="M34.19 55.826h3.891c3.107 0 4.186.682 4.553 1.089.607.674.723 2.097.331 4.112-.439 2.257-1.253 3.858-2.42 4.756-1.194.92-3.138 1.386-5.773 1.386h-2.786l2.205-11.342zm6.674-8.1H26.731a1.39 1.39 0 0 0-1.364 1.123L18.81 82.588a1.39 1.39 0 0 0 1.363 1.653h7.35a1.39 1.39 0 0 0 1.363-1.124l1.525-7.846h5.151c2.912 0 5.364-.318 7.287-.944 1.977-.642 3.796-1.731 5.406-3.237a16.522 16.522 0 0 0 3.259-4.087c.831-1.487 1.429-3.147 1.775-4.931.86-4.423.161-7.964-2.076-10.524-2.216-2.537-5.698-3.823-10.349-3.823zM30.301 68.557h4.471c2.963 0 5.17-.557 6.62-1.675 1.451-1.116 2.428-2.98 2.938-5.591.485-2.508.264-4.277-.665-5.308-.931-1.03-2.791-1.546-5.584-1.546h-5.036l-2.743 14.12m10.563-19.445c4.252 0 7.353 1.117 9.303 3.348 1.95 2.232 2.536 5.347 1.76 9.346-.322 1.648-.863 3.154-1.625 4.518-.764 1.366-1.76 2.614-2.991 3.747-1.468 1.373-3.097 2.352-4.892 2.935-1.794.584-4.08.875-6.857.875h-6.296l-1.743 8.97h-7.35l6.558-33.739h14.133"/><path d="M69.459 74.577a.694.694 0 0 1-.682-.827l2.9-14.928c.277-1.42.209-2.438-.19-2.87-.245-.263-.979-.704-3.15-.704h-5.256l-3.646 18.768a.695.695 0 0 1-.683.56h-7.29a.695.695 0 0 1-.683-.826l6.558-33.739a.695.695 0 0 1 .682-.561h7.29a.695.695 0 0 1 .683.826L64.41 48.42h5.653c4.307 0 7.227.758 8.928 2.321 1.733 1.593 2.275 4.14 1.608 7.573l-3.051 15.702a.695.695 0 0 1-.682.56h-7.407z"/><path fill="#fff" d="M65.31 38.755h-7.291a1.39 1.39 0 0 0-1.364 1.124l-6.557 33.738a1.39 1.39 0 0 0 1.363 1.654h7.291a1.39 1.39 0 0 0 1.364-1.124l3.537-18.205h4.682c2.168 0 2.624.463 2.641.484.132.14.305.795.019 2.264l-2.9 14.927a1.39 1.39 0 0 0 1.364 1.654h7.408a1.39 1.39 0 0 0 1.363-1.124l3.051-15.7c.715-3.686.103-6.45-1.82-8.217-1.836-1.686-4.91-2.505-9.398-2.505h-4.81l1.421-7.315a1.39 1.39 0 0 0-1.364-1.655zm0 1.39-1.743 8.968h6.496c4.087 0 6.907.714 8.457 2.14 1.553 1.426 2.017 3.735 1.398 6.93l-3.052 15.699h-7.407l2.901-14.928c.33-1.698.208-2.856-.365-3.474-.573-.617-1.793-.926-3.658-.926h-5.829l-3.756 19.327H51.46l6.558-33.739h7.292z"/><path d="M92.136 67.864c2.793 0 4.878-.515 6.198-1.53 1.304-1.006 2.206-2.747 2.679-5.175.44-2.27.273-3.854-.5-4.71-.788-.874-2.493-1.317-5.067-1.317h-4.463l-2.475 12.732zM77.54 83.547a.694.694 0 0 1-.682-.828l6.557-33.738a.695.695 0 0 1 .682-.561H98.23c4.442 0 7.748 1.206 9.826 3.585 2.089 2.39 2.734 5.734 1.917 9.935a15.878 15.878 0 0 1-1.699 4.724 15.838 15.838 0 0 1-3.128 3.92c-1.53 1.432-3.265 2.472-5.147 3.083-1.852.604-4.232.91-7.071.91h-5.723l-1.633 8.408a.695.695 0 0 1-.683.562z"/><path fill="#fff" d="M91.555 55.826h3.891c3.107 0 4.186.682 4.552 1.089.61.674.724 2.097.333 4.112-.44 2.257-1.254 3.858-2.421 4.756-1.195.92-3.139 1.386-5.773 1.386h-2.786l2.204-11.342zm6.674-8.1H84.096a1.39 1.39 0 0 0-1.363 1.123l-6.558 33.739a1.39 1.39 0 0 0 1.364 1.653h7.35a1.39 1.39 0 0 0 1.363-1.124l1.525-7.846h5.15c2.911 0 5.364-.318 7.286-.944 1.978-.642 3.797-1.731 5.408-3.238a16.52 16.52 0 0 0 3.258-4.086c.832-1.487 1.428-3.147 1.775-4.931.86-4.423.162-7.964-2.076-10.524-2.216-2.537-5.697-3.823-10.35-3.823zM87.666 68.557h4.47c2.964 0 5.17-.557 6.622-1.675 1.45-1.116 2.428-2.98 2.936-5.591.487-2.508.266-4.277-.665-5.308-.93-1.03-2.791-1.546-5.583-1.546h-5.035Zm10.563-19.445c4.251 0 7.354 1.117 9.303 3.348 1.95 2.232 2.537 5.347 1.759 9.346-.32 1.648-.862 3.154-1.624 4.518-.763 1.366-1.76 2.614-2.992 3.747-1.467 1.373-3.097 2.352-4.892 2.935-1.793.584-4.078.875-6.856.875h-6.295l-1.745 8.97h-7.35l6.558-33.739h14.133"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(84.04136 0 0 84.04136 38.426 42.169)" gradientUnits="userSpaceOnUse"><stop stop-color="#AEB2D5"/><stop offset=".3" stop-color="#AEB2D5"/><stop offset=".75" stop-color="#484C89"/><stop offset="1" stop-color="#484C89"/></radialGradient></defs></svg>
|
||||
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><linearGradient id="python-original-a" gradientUnits="userSpaceOnUse" x1="70.252" y1="1237.476" x2="170.659" y2="1151.089" gradientTransform="matrix(.563 0 0 -.568 -29.215 707.817)"><stop offset="0" stop-color="#5A9FD4"/><stop offset="1" stop-color="#306998"/></linearGradient><linearGradient id="python-original-b" gradientUnits="userSpaceOnUse" x1="209.474" y1="1098.811" x2="173.62" y2="1149.537" gradientTransform="matrix(.563 0 0 -.568 -29.215 707.817)"><stop offset="0" stop-color="#FFD43B"/><stop offset="1" stop-color="#FFE873"/></linearGradient><path fill="url(#python-original-a)" d="M63.391 1.988c-4.222.02-8.252.379-11.8 1.007-10.45 1.846-12.346 5.71-12.346 12.837v9.411h24.693v3.137H29.977c-7.176 0-13.46 4.313-15.426 12.521-2.268 9.405-2.368 15.275 0 25.096 1.755 7.311 5.947 12.519 13.124 12.519h8.491V67.234c0-8.151 7.051-15.34 15.426-15.34h24.665c6.866 0 12.346-5.654 12.346-12.548V15.833c0-6.693-5.646-11.72-12.346-12.837-4.244-.706-8.645-1.027-12.866-1.008zM50.037 9.557c2.55 0 4.634 2.117 4.634 4.721 0 2.593-2.083 4.69-4.634 4.69-2.56 0-4.633-2.097-4.633-4.69-.001-2.604 2.073-4.721 4.633-4.721z" transform="translate(0 10.26)"/><path fill="url(#python-original-b)" d="M91.682 28.38v10.966c0 8.5-7.208 15.655-15.426 15.655H51.591c-6.756 0-12.346 5.783-12.346 12.549v23.515c0 6.691 5.818 10.628 12.346 12.547 7.816 2.297 15.312 2.713 24.665 0 6.216-1.801 12.346-5.423 12.346-12.547v-9.412H63.938v-3.138h37.012c7.176 0 9.852-5.005 12.348-12.519 2.578-7.735 2.467-15.174 0-25.096-1.774-7.145-5.161-12.521-12.348-12.521h-9.268zM77.809 87.927c2.561 0 4.634 2.097 4.634 4.692 0 2.602-2.074 4.719-4.634 4.719-2.55 0-4.633-2.117-4.633-4.719 0-2.595 2.083-4.692 4.633-4.692z" transform="translate(0 10.26)"/><radialGradient id="python-original-c" cx="1825.678" cy="444.45" r="26.743" gradientTransform="matrix(0 -.24 -1.055 0 532.979 557.576)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#B8B8B8" stop-opacity=".498"/><stop offset="1" stop-color="#7F7F7F" stop-opacity="0"/></radialGradient><path opacity=".444" fill="url(#python-original-c)" d="M97.309 119.597c0 3.543-14.816 6.416-33.091 6.416-18.276 0-33.092-2.873-33.092-6.416 0-3.544 14.815-6.417 33.092-6.417 18.275 0 33.091 2.872 33.091 6.417z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><linearGradient id="a" x1=".741" x2="590.86" y1="3.666" y2="593.79" gradientTransform="matrix(.2169 0 0 .14527 -.16 14.112)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#cbced0"/><stop offset="1" stop-color="#84838b"/></linearGradient><linearGradient id="b" x1="301.03" x2="703.07" y1="151.4" y2="553.44" gradientTransform="matrix(.17572 0 0 .17931 -.16 14.112)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#276dc3"/><stop offset="1" stop-color="#165caa"/></linearGradient></defs><path fill="url(#a)" fill-rule="evenodd" d="M64 100.38c-35.346 0-64-19.19-64-42.863 0-23.672 28.654-42.863 64-42.863s64 19.19 64 42.863c0 23.672-28.654 42.863-64 42.863zm9.796-68.967c-26.866 0-48.646 13.119-48.646 29.303 0 16.183 21.78 29.303 48.646 29.303s46.693-8.97 46.693-29.303c0-20.327-19.827-29.303-46.693-29.303z"/><path fill="url(#b)" fill-rule="evenodd" d="M97.469 81.033s3.874 1.169 6.124 2.308c.78.395 2.132 1.183 3.106 2.219a8.388 8.388 0 0 1 1.42 2.04l15.266 25.74-24.674.01-11.537-21.666s-2.363-4.06-3.817-5.237c-1.213-.982-1.73-1.331-2.929-1.331h-5.862l.004 28.219-21.833.009V41.26h43.844s19.97.36 19.97 19.359c0 18.999-19.082 20.413-19.082 20.413zm-9.497-24.137-13.218-.009-.006 12.258 13.224-.005s6.124-.019 6.124-6.235c0-6.34-6.124-6.009-6.124-6.009z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#d91404" d="m35.971 111.33 81.958 11.188c-9.374-15.606-18.507-30.813-27.713-46.144Zm89.71-86.383L93.513 73.339c-.462.696-1.061 1.248-.41 2.321 8.016 13.237 15.969 26.513 23.942 39.777 1.258 2.095 2.53 4.182 4.157 6.192l4.834-96.58zM16.252 66.22c.375.355 1.311.562 1.747.347 7.689-3.779 15.427-7.474 22.948-11.564 2.453-1.333 4.339-3.723 6.452-5.661 6.997-6.417 13.983-12.847 20.966-19.278.427-.395.933-.777 1.188-1.275 2.508-4.902 4.973-9.829 7.525-14.898-3.043-1.144-5.928-2.263-8.849-3.281-.396-.138-1.02.136-1.449.375-6.761 3.777-13.649 7.353-20.195 11.472-3.275 2.061-5.943 5.098-8.843 7.743-4.674 4.266-9.342 8.542-13.948 12.882a24.011 24.011 0 0 0-3.288 3.854c-3.15 4.587-6.206 9.24-9.402 14.025 1.786 1.847 3.41 3.613 5.148 5.259zm28.102-6.271-11.556 48.823 54.3-34.987zm76.631-34.846-46.15 7.71 15.662 38.096zM44.996 56.644l41.892 13.6c-5.25-12.79-10.32-25.133-15.495-37.737ZM16.831 75.643 2.169 110.691l27.925-.825Zm13.593 26.096.346-.076c3.353-13.941 6.754-27.786 10.177-42.272L18.544 71.035c3.819 9.926 7.891 20.397 11.88 30.704zm84.927-78.897c-4.459-1.181-8.918-2.366-13.379-3.539-6.412-1.686-12.829-3.351-19.237-5.052-.801-.213-1.38-.352-1.851.613-2.265 4.64-4.6 9.245-6.901 13.868-.071.143-.056.328-.111.687l41.47-6.285zM89.482 12.288l36.343 10.054-6.005-17.11-30.285 6.715ZM33.505 114.007c-4.501-.519-9.122-.042-13.687.037-3.75.063-7.5.206-11.25.323-.386.012-.771.09-1.156.506 31.003 2.866 62.005 5.732 93.007 8.6l.063-.414-29.815-4.07c-12.384-1.691-24.747-3.551-37.162-4.982ZM2.782 99.994c3.995-9.27 7.973-18.546 11.984-27.809.401-.929.37-1.56-.415-2.308-1.678-1.597-3.237-3.318-5.071-5.226-2.479 12.24-4.897 24.177-7.317 36.113l.271.127c.185-.297.411-.578.548-.897zm78.74-90.153c6.737-1.738 13.572-3.097 20.367-4.613.44-.099.87-.244 1.303-.368l-.067-.332-29.194 3.928c2.741 1.197 4.853 2.091 7.591 1.385z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path d="M62.96.242c-.232.135-1.203 1.528-2.16 3.097-2.4 3.94-2.426 3.942-5.65.55-2.098-2.208-2.605-2.612-3.28-2.607-.44.002-.995.152-1.235.332-.24.18-.916 1.612-1.504 3.183-1.346 3.6-1.41 3.715-2.156 3.86-.46.086-1.343-.407-3.463-1.929-1.565-1.125-3.1-2.045-3.411-2.045-1.291 0-1.655.706-2.27 4.4-.78 4.697-.754 4.681-4.988 2.758-1.71-.776-3.33-1.41-3.603-1.41-.274 0-.792.293-1.15.652-.652.652-.653.655-.475 4.246l.178 3.595-.68.364c-.602.322-1.017.283-3.684-.348-3.48-.822-4.216-.8-4.92.15l-.516.693.692 2.964c.38 1.63.745 3.2.814 3.487.067.287-.05.746-.26 1.02-.348.448-.717.49-3.94.44-5.452-.086-5.761.382-3.51 5.3.718 1.56 1.305 2.98 1.305 3.15 0 .898-.717 1.224-3.794 1.727-1.722.28-3.218.51-3.326.51-.107 0-.43.235-.717.522-.937.936-.671 1.816 1.453 4.814 2.646 3.735 2.642 3.75-1.73 5.421-4.971 1.902-5.072 2.37-1.287 5.96 3.525 3.344 3.53 3.295-.461 5.804C.208 62.8.162 62.846.085 63.876c-.093 1.253-.071 1.275 3.538 3.48 3.57 2.18 3.57 2.246.067 5.56C-.078 76.48.038 77 5.013 78.877c4.347 1.64 4.353 1.66 1.702 5.394-1.502 2.117-1.981 3-1.981 3.653 0 1.223.637 1.535 4.44 2.174 3.206.54 3.92.857 3.92 1.741 0 .182-.588 1.612-1.307 3.177-2.236 4.87-1.981 5.275 3.31 5.275 4.93 0 4.799-.15 3.737 4.294-.8 3.35-.813 3.992-.088 4.715.554.556 1.6.494 4.87-.289 2.499-.596 2.937-.637 3.516-.328l.66.354-.177 3.594c-.178 3.593-.177 3.595.475 4.248.358.36.884.652 1.165.652.282 0 1.903-.63 3.604-1.404 4.22-1.916 4.194-1.932 4.973 2.75.617 3.711.977 4.4 2.294 4.4.327 0 1.83-.88 3.34-1.958 2.654-1.893 3.342-2.19 4.049-1.74.182.115.89 1.67 1.572 3.455 1.003 2.625 1.37 3.31 1.929 3.576 1.062.51 1.72.1 4.218-2.62 3.016-3.286 3.14-3.27 5.602.72 2.72 4.406 3.424 4.396 6.212-.089 2.402-3.864 2.374-3.862 5.621-.47 2.157 2.25 2.616 2.61 3.343 2.61.464 0 1.019-.175 1.23-.388.214-.213.92-1.786 1.568-3.496.649-1.71 1.321-3.2 1.495-3.31.687-.436 1.398-.13 4.048 1.752 1.56 1.108 3.028 1.96 3.377 1.96 1.296 0 1.764-.92 2.302-4.535.46-3.082.554-3.378 1.16-3.685.596-.302.954-.2 3.75 1.07 1.701.77 3.323 1.402 3.604 1.402.282 0 .816-.302 1.184-.672l.672-.67-.184-3.448c-.177-3.29-.16-3.468.364-3.943.54-.488.596-.486 3.615.204 3.656.835 4.338.857 5.025.17.671-.67.664-.818-.254-4.69-1.03-4.346-1.168-4.19 3.78-4.19 3.374 0 3.75-.049 4.18-.523.718-.793.547-1.702-.896-4.779-.729-1.55-1.32-2.96-1.315-3.135.024-.914.743-1.227 4.065-1.767 2.033-.329 3.553-.71 3.829-.96.923-.833.584-1.918-1.523-4.873-2.642-3.703-2.63-3.738 1.599-5.297 5.064-1.866 5.209-2.488 1.419-6.09-3.51-3.335-3.512-3.317.333-5.677 4.648-2.853 4.655-3.496.082-6.335-3.933-2.44-3.93-2.406-.405-5.753 3.78-3.593 3.678-4.063-1.295-5.965-4.388-1.679-4.402-1.72-1.735-5.38 1.588-2.18 1.982-2.903 1.982-3.65 0-1.306-.586-1.598-4.436-2.22-3.216-.52-3.924-.835-3.924-1.75 0-.174.588-1.574 1.307-3.113 1.406-3.013 1.604-4.22.808-4.94-.428-.387-1-.443-4.067-.392-3.208.054-3.618.008-4.063-.439-.486-.488-.48-.557.278-3.725.931-3.88.935-3.975.17-4.694-.777-.73-1.262-.718-4.826.121-2.597.612-3.027.653-3.617.337l-.67-.36.185-3.582.186-3.58-.67-.67c-.369-.37-.891-.67-1.163-.67-.27 0-1.884.64-3.583 1.421-2.838 1.306-3.143 1.393-3.757 1.072-.612-.32-.714-.637-1.237-3.829-.603-3.693-.977-4.412-2.288-4.412-.311 0-1.853.925-3.426 2.055-2.584 1.856-2.93 2.032-3.574 1.807-.533-.186-.843-.59-1.221-1.599-.28-.742-.817-2.172-1.194-3.177-.762-2.028-1.187-2.482-2.328-2.482-.637 0-1.213.458-3.28 2.604-3.25 3.375-3.261 3.374-5.65-.545C66.073 1.78 65.075.382 64.81.24c-.597-.32-1.3-.32-1.85.002m2.96 11.798c2.83 2.014 1.326 6.75-2.144 6.75-3.368 0-5.064-4.057-2.66-6.36 1.358-1.3 3.304-1.459 4.805-.39m-3.558 12.507c1.855.705 2.616.282 6.852-3.8l3.182-3.07 1.347.18c4.225.56 12.627 4.25 17.455 7.666 4.436 3.14 10.332 9.534 12.845 13.93l.537.942-2.38 5.364c-1.31 2.95-2.382 5.673-2.382 6.053 0 .878.576 2.267 1.13 2.726.234.195 2.457 1.265 4.939 2.378l4.51 2.025.178 1.148c.23 1.495.26 5.167.052 6.21l-.163.816h-2.575c-2.987 0-2.756-.267-2.918 3.396-.118 2.656-.76 4.124-2.22 5.075-2.377 1.551-6.304 1.27-7.97-.57-.255-.284-.752-1.705-1.105-3.16-1.03-4.254-2.413-6.64-5.193-8.965-.878-.733-1.595-1.418-1.595-1.522 0-.102.965-.915 2.145-1.803 4.298-3.24 6.77-7.012 7.04-10.747.519-7.126-5.158-13.767-13.602-15.92-2.002-.51-2.857-.526-27.624-.526-14.057 0-25.56-.092-25.56-.204 0-.263 3.125-3.295 4.965-4.816 5.054-4.178 11.618-7.465 18.417-9.22l2.35-.61 3.34 3.387c1.839 1.863 3.64 3.5 4.003 3.637M20.3 46.34c1.539 1.008 2.17 3.54 1.26 5.062-1.405 2.356-4.966 2.455-6.373.178-2.046-3.309 1.895-7.349 5.113-5.24m90.672.13c4.026 2.454.906 8.493-3.404 6.586-2.877-1.273-2.97-5.206-.155-6.64 1.174-.6 2.523-.579 3.56.053M32.163 61.5v15.02h-13.28l-.526-2.285c-1.036-4.5-1.472-9.156-1.211-12.969l.182-2.679 4.565-2.047c2.864-1.283 4.706-2.262 4.943-2.625 1.038-1.584.94-2.715-.518-5.933l-.68-1.502h6.523V61.5M70.39 47.132c2.843.74 4.345 2.245 4.349 4.355.002 1.55-.765 2.52-2.67 3.38-1.348.61-1.562.625-10.063.708l-8.686.084v-8.92h7.782c6.078 0 8.112.086 9.288.393m-2.934 21.554c1.41.392 3.076 1.61Line truncated
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#f05138" d="M126.33 34.06a39.32 39.32 0 00-.79-7.83 28.78 28.78 0 00-2.65-7.58 28.84 28.84 0 00-4.76-6.32 23.42 23.42 0 00-6.62-4.55 27.27 27.27 0 00-7.68-2.53c-2.65-.51-5.56-.51-8.21-.76H30.25a45.46 45.46 0 00-6.09.51 21.82 21.82 0 00-5.82 1.52c-.53.25-1.32.51-1.85.76a33.82 33.82 0 00-5 3.28c-.53.51-1.06.76-1.59 1.26a22.41 22.41 0 00-4.76 6.32 23.61 23.61 0 00-2.65 7.58 78.5 78.5 0 00-.79 7.83v60.39a39.32 39.32 0 00.79 7.83 28.78 28.78 0 002.65 7.58 28.84 28.84 0 004.76 6.32 23.42 23.42 0 006.62 4.55 27.27 27.27 0 007.68 2.53c2.65.51 5.56.51 8.21.76h63.22a45.08 45.08 0 008.21-.76 27.27 27.27 0 007.68-2.53 30.13 30.13 0 006.62-4.55 22.41 22.41 0 004.76-6.32 23.61 23.61 0 002.65-7.58 78.49 78.49 0 00.79-7.83V34.06z"/><path fill="#fefefe" d="M85 96.5c-11.11 6.13-26.38 6.76-41.75.47A64.53 64.53 0 0113.84 73a50 50 0 0010.85 6.32c15.87 7.1 31.73 6.61 42.9 0-15.9-11.66-29.4-26.82-39.46-39.2a43.47 43.47 0 01-5.29-6.82c12.16 10.61 31.5 24 38.38 27.79a271.77 271.77 0 01-27-32.34 266.8 266.8 0 0044.47 34.87c.71.38 1.26.7 1.7 1a32.7 32.7 0 001.21-3.51c3.71-12.89-.53-27.54-9.79-39.67C93.25 33.81 106 57.05 100.66 76.51c-.14.53-.29 1-.45 1.55l.19.22c10.59 12.63 7.68 26 6.35 23.5C101 91 90.37 94.33 85 96.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#00ADEF" d="M126 1.637l-67 9.834v49.831l67-.534zM1.647 66.709l.003 42.404 50.791 6.983-.04-49.057zm56.82.68l.094 49.465 67.376 9.509.016-58.863zM1.61 19.297l.047 42.383 50.791-.289-.023-49.016z"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |