Compare commits
43 Commits
v1.0.0-sta
...
staging
Author | SHA1 | Date | |
---|---|---|---|
|
4cfb73b2d4 | ||
7ecfb97df5 | |||
bb39ae856d | |||
170bdae725 | |||
eba92ed64b | |||
92787448bb | |||
bf1729cf0d | |||
f0b22f6a06 | |||
4e707008f8 | |||
20ab889cf8 | |||
4add77856e | |||
791551a4e8 | |||
63862b19c0 | |||
376c0fd041 | |||
207d3483ca | |||
0ce950c9c8 | |||
cdc8cf2b05 | |||
02ee112de4 | |||
aa2fb4f5b9 | |||
0f8b6c6b29 | |||
94af8462d3 | |||
d020552af5 | |||
f53a797169 | |||
8dec198afe | |||
3bed3e0578 | |||
fee0b4e681 | |||
61914d2392 | |||
3de838dded | |||
91e1f18575 | |||
|
596593ef6a | ||
189a84b69f | |||
5aa83510e1 | |||
|
7d7ef10226 | ||
33b57bf173 | |||
90a8a50ad0 | |||
74a3148e92 | |||
89ec7443a0 | |||
ccd44c10fa | |||
867fc131b1 | |||
624d235a0e | |||
90abfb6de8 | |||
0ee7b35530 | |||
6c717e5768 |
@ -1 +1,8 @@
|
||||
WEBSITE_PORT=5000
|
||||
API_PORT=5500
|
||||
|
||||
DATABASE_USER=wikipedia_user
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_NAME=wikipedia
|
||||
DATABASE_HOST=127.0.0.1
|
||||
DATABASE_PORT=5432
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -24,15 +24,19 @@ jobs:
|
||||
- name: "Install dependencies"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- run: "cp .env.example .env"
|
||||
- run: "cp apps/website/.env.example apps/website/.env"
|
||||
- run: "cp apps/api/.env.example apps/api/.env"
|
||||
|
||||
# - name: "Install Playwright"
|
||||
# run: "pnpm exec playwright install --with-deps"
|
||||
|
||||
- run: "node --run build"
|
||||
- run: "node --run test"
|
||||
- run: "node --run lint:editorconfig"
|
||||
- run: "node --run lint:prettier"
|
||||
- run: "node --run lint:eslint"
|
||||
- run: "node --run lint:typescript"
|
||||
- run: "node --run test"
|
||||
- run: "node --run build"
|
||||
|
||||
commitlint:
|
||||
runs-on: "ubuntu-latest"
|
||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -20,7 +20,15 @@ build/
|
||||
.DS_Store
|
||||
*.pem
|
||||
.turbo
|
||||
bin/
|
||||
tmp/
|
||||
.adonisjs/
|
||||
|
||||
# data
|
||||
data/dump
|
||||
data/sql-pages-inserts/*
|
||||
!data/sql-pages-inserts/0000-pages.sh
|
||||
data/sql-internal-links-inserts/*
|
||||
!data/sql-internal-links-inserts/0000-internal-links.sh
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@ -50,4 +58,4 @@ storybook-static
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
# next-env.d.ts
|
||||
next-env.d.ts
|
||||
|
36
.vscode/adonis.code-snippets
vendored
Normal file
36
.vscode/adonis.code-snippets
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"Adonis Controller and Route": {
|
||||
"scope": "typescript",
|
||||
"prefix": "apic",
|
||||
"body": [
|
||||
"import { throttle } from \"#start/limiter.ts\"",
|
||||
"import type { HttpContext } from \"@adonisjs/core/http\"",
|
||||
"import router from \"@adonisjs/core/services/router\"",
|
||||
"import vine from \"@vinejs/vine\"",
|
||||
"",
|
||||
"export const ${1:controller_name}_validator = vine.compile(",
|
||||
" vine.object({})",
|
||||
")",
|
||||
"",
|
||||
"export default class ${1:controller_name} {",
|
||||
" public async handle(context: HttpContext): Promise<{",
|
||||
" __response: unknown",
|
||||
" __status: 200",
|
||||
" }> {",
|
||||
" const payload = await context.request.validateUsing(${1:controller_name}_validator)",
|
||||
" return context.response.ok(payload)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"router",
|
||||
" .get(\"${2:/router-path}\", [${1:controller_name}])",
|
||||
" .use(throttle)",
|
||||
" .openapi({",
|
||||
" description: \"Description.\",",
|
||||
" tags: [\"tag\"],",
|
||||
" })",
|
||||
"",
|
||||
],
|
||||
"description": "Adonis Controller and Route",
|
||||
},
|
||||
}
|
4
.vscode/react.code-snippets
vendored
4
.vscode/react.code-snippets
vendored
@ -3,7 +3,7 @@
|
||||
"scope": "typescriptreact",
|
||||
"prefix": "rfc",
|
||||
"body": [
|
||||
"interface ${1:ComponentName}Props {}",
|
||||
"export interface ${1:ComponentName}Props {}",
|
||||
"",
|
||||
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
|
||||
" return (",
|
||||
@ -22,7 +22,7 @@
|
||||
"body": [
|
||||
"import type { Meta, StoryObj } from \"@storybook/react\"",
|
||||
"",
|
||||
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}\"",
|
||||
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
|
||||
"",
|
||||
"const meta = {",
|
||||
" title: \"${1:ComponentName}\",",
|
||||
|
36
README.md
36
README.md
@ -1,7 +1,11 @@
|
||||
# Wikipedia Game Solver
|
||||
|
||||
> \[!IMPORTANT\]
|
||||
> This project is a work in progress, at an early stage of development.
|
||||
> The project is **abandoned and not maintained anymore**.
|
||||
>
|
||||
> It was a **proof of concept** to deal with large amounts of data in databases, and have some fun dealing with Wikipedia data.
|
||||
> It was also a way to learn and experiment with a clean monorepo architecture, with [Turborepo](https://turbo.build/repo), and [TypeScript](https://www.typescriptlang.org/) as the main language.
|
||||
> The project setup **can be reused as a template/boilerplate for future projects**.
|
||||
|
||||
## About
|
||||
|
||||
@ -9,19 +13,15 @@ The Wikipedia Game involves players competing to navigate from one [Wikipedia](h
|
||||
|
||||
[**Wikipedia Game Solver**](https://wikipedia-game-solver.theoludwig.fr) is a tool that helps you find the shortest path between two Wikipedia pages, using only internal links, basically solving the Wikipedia Game for you.
|
||||
|
||||
Available online: <https://wikipedia-game-solver.theoludwig.fr>
|
||||
|
||||
> \[!NOTE\]
|
||||
> The project is also a way to learn and experiment with a monorepo architecture, with [Turborepo](https://turbo.build/repo), and [TypeScript](https://www.typescriptlang.org/) as the main language.
|
||||
>
|
||||
> The project setup **can be used as a template/boilerplate for new projects**.
|
||||
~~Available online: <https://wikipedia-game-solver.theoludwig.fr>~~ => **Offline** as the project is abandoned.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 22.0.0
|
||||
- [pnpm](https://pnpm.io/) >= 9.5.0
|
||||
- [pnpm](https://pnpm.io/) >= 9.9.0
|
||||
- [Docker](https://www.docker.com/)
|
||||
|
||||
### Installation
|
||||
|
||||
@ -32,6 +32,7 @@ cd wikipedia-game-solver
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
cp apps/website/.env.example apps/website/.env
|
||||
cp apps/api/.env.example apps/api/.env
|
||||
|
||||
# Install dependencies
|
||||
pnpm install --frozen-lockfile
|
||||
@ -43,9 +44,15 @@ pnpm exec playwright install --with-deps
|
||||
### Development
|
||||
|
||||
```sh
|
||||
# Start the development server
|
||||
# Start the development servers
|
||||
node --run dev
|
||||
|
||||
# Start the development Docker services (e.g: Database)
|
||||
docker compose --file compose.dev.yaml up
|
||||
|
||||
# Database migrations
|
||||
node --run database:migrate
|
||||
|
||||
# Lint
|
||||
node --run lint:editorconfig
|
||||
node --run lint:prettier
|
||||
@ -57,6 +64,10 @@ node --run test
|
||||
|
||||
# Build
|
||||
node --run build
|
||||
|
||||
# To execute a command in a specific package (e.g: apps/api)
|
||||
cd apps/api
|
||||
node --run ace -- list
|
||||
```
|
||||
|
||||
### Production environment with [Docker](https://www.docker.com/)
|
||||
@ -64,11 +75,16 @@ node --run build
|
||||
```sh
|
||||
# Setup and run all the services for you
|
||||
docker compose up --build
|
||||
|
||||
# To execute database migrations
|
||||
docker compose exec wikipedia-game-solver-api sh
|
||||
node --run database:migrate
|
||||
```
|
||||
|
||||
#### Services started
|
||||
|
||||
`wikipedia-game-solver`: <http://127.0.0.1:5000>
|
||||
- `apps/website`: <http://127.0.0.1:5000>
|
||||
- `apps/api`: <http://127.0.0.1:5500>
|
||||
|
||||
## License
|
||||
|
||||
|
60
TODO.md
60
TODO.md
@ -1,18 +1,62 @@
|
||||
# TODO
|
||||
|
||||
- [x] chore: initial commit (+ mirror on GitHub)
|
||||
- [x] chore: initial commit
|
||||
- [x] Deploy first staging version (v1.0.0-staging.1)
|
||||
- [ ] Add docs to add locale/edit translations, create component, install a dependency in a package, create a new package, technology used, architecture, links where it's deployed, how to use/install for end users, how to update dependencies with `npx taze -l` etc.
|
||||
- [ ] Implement Wikipedia Game Solver (`website`) with inputs, button to submit, and list all articles to go from one to another, or none if it is not possible
|
||||
- [ ] v1.0.0-staging.2
|
||||
- [x] Wikipedia Database Dump
|
||||
- [x] Download SQL files
|
||||
- [x] Extract SQL files
|
||||
- [x] Tables structure `CREATE TABLE`
|
||||
- [x] `page.sql` (`pages` table)
|
||||
- [x] `pagelinks.sql` (`internal_links` table)
|
||||
- [x] Adapt downloaded SQL files
|
||||
- [x] `page.sql` (`pages` table)
|
||||
- [x] `pagelinks.sql` (`internal_links` table)
|
||||
- [x] Import SQL files
|
||||
- [x] Try `SELECT count(*) FROM internal_links il WHERE il.from_page_id = (SELECT p.id FROM pages p WHERE p.title = 'Linux'); -- Count of internal links for 'Linux' page`
|
||||
- [x] Try:
|
||||
```sql
|
||||
SELECT il.to_page_id, pl.title
|
||||
FROM internal_links il
|
||||
JOIN pages pl ON pl.id = il.to_page_id
|
||||
WHERE il.from_page_id = (
|
||||
SELECT p.id FROM pages p WHERE p.title = 'Node.js'
|
||||
);
|
||||
```
|
||||
- [ ] Move from POC (Proof of concept) in `data` folder to `apps/cli` folder
|
||||
- [ ] Documentation how to use + Last execution date
|
||||
- [ ] Rewrite bash script to download and extract SQL files from Wikipedia Database Dump to Node.js for better cross-platform support and easier maintenance + automation, preferably one Node.js script to generate everything to create the database
|
||||
- [ ] Verify file content up to before inserts, to check if it matches last version, and diff with last version
|
||||
- [ ] Update logic to create custom `internal_links` table to make it work with latest wikipedia dumps (notably concerning the change in `pagelinks.sql` where the title is not included anymore, but instead it uses `pl_target_id`, foreign key to `linktarget`), last tested dumb working `20240420`
|
||||
- [ ] Handle redirects
|
||||
- [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/)) to get shortest paths between 2 pages
|
||||
- [x] Init AdonisJS project
|
||||
- [x] Create Lucid models and migrations for Wikipedia Database Dump: `pages` and `internal_links` tables
|
||||
- [x] Implement `GET /wikipedia/pages?title=Node.js` to search a page by title (not necessarily with the title sanitized, search with input by user to check if page exists)
|
||||
- [x] Implement `GET /wikipedia/pages/[id]` to get a page and all its internal links with the pageId
|
||||
- [ ] Implement `GET /wikipedia/shortest-paths?fromPageId=id&toPageId=id` to get all the possible paths between 2 pages (e.g: `Node.js` `26415635` => `Linux` `6097297`)
|
||||
- [x] Setup tests with database + add coverage
|
||||
- [x] Setup Health checks
|
||||
- [x] Setup Rate limiting
|
||||
- [ ] Share VineJS validators between `website` and `api`
|
||||
- [ ] Implement Wikipedia Game Solver (`website`)
|
||||
- [x] Init Next.js project
|
||||
- [x] Try to use <https://www.npmjs.com/package/@tuyau/client> for API calls
|
||||
- [ ] Implement a form with inputs, button to submit, and list all pages to go from one to another, or none if it is not possible
|
||||
- [ ] Add images, links to the pages + good UI/UX
|
||||
- [ ] Autocompletion page titles
|
||||
- [ ] Implement toast notifications for errors, warnings, and success messages
|
||||
- [ ] Implement CLI (`cli`)
|
||||
- [ ] v1.0.0-staging.3
|
||||
- [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/))
|
||||
- [ ] v1.0.0-staging.4
|
||||
- [ ] v1.0.0
|
||||
- [ ] Init Clipanion project
|
||||
- [ ] Implement `wikipedia-game-solver internal-links --from="Node.js" --to="Linux"` command to get all the possible paths between 2 pages.
|
||||
- [ ] Add docs to add locale/edit translations, create component, install a dependency in a package, create a new package, technology used, architecture, links where it's deployed, how to use/install for end users, how to update dependencies with `npx taze -l major` etc.
|
||||
- [ ] GitHub Mirror
|
||||
- [ ] Delete `TODO.md` file and instead use issues for the remaining tasks
|
||||
|
||||
## Links
|
||||
|
||||
- <https://www.sixdegreesofwikipedia.com/> and <https://github.com/jwngr/sdow>
|
||||
- <https://github.com/shyamupa/wikidump_preprocessing>
|
||||
- <https://www.mediawiki.org/wiki/API:Allpages>
|
||||
- <https://www.thewikigame.com/>
|
||||
- How to get all URLs in a Wikipedia page: <https://stackoverflow.com/questions/14882571/how-to-get-all-urls-in-a-wikipedia-page>
|
||||
- <https://en.wikipedia.org/w/api.php?action=query&titles=Title&prop=links&pllimit=max&format=json>
|
||||
|
4
apps/api/.c8rc.json
Normal file
4
apps/api/.c8rc.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"reporter": ["text", "html", "json"],
|
||||
"exclude": ["adonisrc.ts", "tests/**", "database/**", "config/**", "bin/**"]
|
||||
}
|
16
apps/api/.env.example
Normal file
16
apps/api/.env.example
Normal file
@ -0,0 +1,16 @@
|
||||
TZ=UTC
|
||||
PORT=5500
|
||||
HOST=0.0.0.0
|
||||
API_URL=http://127.0.0.1:5500
|
||||
LOG_LEVEL=info
|
||||
APP_KEY=LFGmw8iGkYF7vfS18ZB9-1Gn-6LfmoAk
|
||||
NODE_ENV=development
|
||||
|
||||
DATABASE_USER=wikipedia_user
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_NAME=wikipedia
|
||||
DATABASE_HOST=127.0.0.1
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_DEBUG=false
|
||||
|
||||
LIMITER_STORE=database
|
20
apps/api/.eslintrc.json
Normal file
20
apps/api/.eslintrc.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@repo/eslint-config"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["app/controllers/**/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
32
apps/api/Dockerfile
Normal file
32
apps/api/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
FROM node:22.4.1-slim AS node-pnpm
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
ENV DO_NOT_TRACK=1
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM node-pnpm AS builder
|
||||
COPY ./ ./
|
||||
RUN pnpm install --global turbo@2.1.0
|
||||
RUN turbo prune @repo/api --docker
|
||||
|
||||
FROM node-pnpm AS installer
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /usr/src/app/out/json/ ./
|
||||
COPY --from=builder /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod
|
||||
COPY --from=builder /usr/src/app/out/full/ ./
|
||||
COPY turbo.json turbo.json
|
||||
# RUN pnpm --filter=@repo/api... exec turbo run build
|
||||
|
||||
FROM node-pnpm AS runner
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
|
||||
USER applicationrunner
|
||||
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app ./
|
||||
WORKDIR /usr/src/app/apps/api
|
||||
RUN node --run tuyau
|
||||
CMD ["node", "--import=tsx", "./bin/server.ts"]
|
74
apps/api/adonisrc.ts
Normal file
74
apps/api/adonisrc.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { defineConfig } from "@adonisjs/core/app"
|
||||
|
||||
export default defineConfig({
|
||||
commands: [
|
||||
async () => {
|
||||
return await import("@adonisjs/core/commands")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/lucid/commands")
|
||||
},
|
||||
async () => {
|
||||
return await import("@tuyau/core/commands")
|
||||
},
|
||||
async () => {
|
||||
return await import("@tuyau/openapi/commands")
|
||||
},
|
||||
],
|
||||
providers: [
|
||||
async () => {
|
||||
return await import("@adonisjs/core/providers/app_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/core/providers/hash_provider")
|
||||
},
|
||||
{
|
||||
file: async () => {
|
||||
return await import("@adonisjs/core/providers/repl_provider")
|
||||
},
|
||||
environment: ["repl", "test"],
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/core/providers/vinejs_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/cors/cors_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/lucid/database_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/auth/auth_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/limiter/limiter_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@tuyau/core/tuyau_provider")
|
||||
},
|
||||
async () => {
|
||||
return await import("@tuyau/openapi/openapi_provider")
|
||||
},
|
||||
],
|
||||
preloads: [
|
||||
async () => {
|
||||
return await import("#start/database.ts")
|
||||
},
|
||||
async () => {
|
||||
return await import("#start/routes.ts")
|
||||
},
|
||||
async () => {
|
||||
return await import("#start/kernel.ts")
|
||||
},
|
||||
],
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ["**/*.test.ts"],
|
||||
name: "functional",
|
||||
timeout: 30_000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
})
|
@ -0,0 +1,33 @@
|
||||
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.ts"
|
||||
import { test } from "@japa/runner"
|
||||
|
||||
test.group("GET /health", () => {
|
||||
test("should succeeds and get `isHealthy: true`", async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
|
||||
// Act - When
|
||||
const response = await client
|
||||
.get("/health")
|
||||
.header(APP_KEY_HEADER_NAME, APP_KEY)
|
||||
const responseBody = response.body()
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(200)
|
||||
assert.equal(responseBody.isHealthy, true)
|
||||
})
|
||||
|
||||
test("should fails and unauthorized when the app key is not provided", async ({
|
||||
client,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
|
||||
// Act - When
|
||||
const response = await client.get("/health")
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(401)
|
||||
})
|
||||
})
|
32
apps/api/app/controllers/health/get_health_controller.ts
Normal file
32
apps/api/app/controllers/health/get_health_controller.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { healthChecks } from "#start/health.ts"
|
||||
import { middleware } from "#start/kernel.ts"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import type { HealthCheckReport } from "@adonisjs/core/types/health"
|
||||
|
||||
export default class get_health_controller {
|
||||
public async handle(context: HttpContext): Promise<
|
||||
| {
|
||||
__response: HealthCheckReport
|
||||
__status: 200
|
||||
}
|
||||
| {
|
||||
__response: HealthCheckReport
|
||||
__status: 503
|
||||
}
|
||||
> {
|
||||
const report = await healthChecks.run()
|
||||
if (!report.isHealthy) {
|
||||
return context.response.serviceUnavailable(report)
|
||||
}
|
||||
return context.response.ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
.get("/health", [get_health_controller])
|
||||
.use(middleware.appKeySecurity())
|
||||
.openapi({
|
||||
description: "Ensure that the application is in a healthy state.",
|
||||
tags: ["health"],
|
||||
})
|
@ -0,0 +1,52 @@
|
||||
import { PageFactory } from "#database/factories/page_factory.ts"
|
||||
import testUtils from "@adonisjs/core/services/test_utils"
|
||||
import db from "@adonisjs/lucid/services/db"
|
||||
import { test } from "@japa/runner"
|
||||
|
||||
test.group("GET /wikipedia/pages/[id]", (group) => {
|
||||
group.each.setup(async () => {
|
||||
return await testUtils.db().truncate()
|
||||
})
|
||||
|
||||
test("should succeeds and get the page with the given id, and get all its internal links", async ({
|
||||
client,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
const page = await PageFactory.create()
|
||||
const pages = await PageFactory.createMany(10)
|
||||
const internalLinksPages = pages.slice(0, 5)
|
||||
await Promise.all(
|
||||
internalLinksPages.map(async (internalLinkPage) => {
|
||||
await db.table("internal_links").insert({
|
||||
from_page_id: page.id,
|
||||
to_page_id: internalLinkPage.id,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Act - When
|
||||
const response = await client.get(`/wikipedia/pages/${page.id}`)
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(200)
|
||||
response.assertBody({
|
||||
...page.toJSON(),
|
||||
internalLinks: internalLinksPages.map((page) => {
|
||||
return page.toJSON()
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
test("should fails with a 404 status code when the page with the given id does not exist", async ({
|
||||
client,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
const page = await PageFactory.create()
|
||||
|
||||
// Act - When
|
||||
const response = await client.get(`/wikipedia/pages/${page.id + 1}`)
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(404)
|
||||
})
|
||||
})
|
@ -0,0 +1,42 @@
|
||||
import type { ExceptionMessage } from "#app/exceptions/handler.ts"
|
||||
import Page, { type PageWithInternalLinksRaw } from "#app/models/page.ts"
|
||||
import { throttle } from "#start/limiter.ts"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import vine from "@vinejs/vine"
|
||||
|
||||
export const get_wikipedia_page_by_id_validator = vine.compile(
|
||||
vine.object({
|
||||
params: vine.object({
|
||||
id: vine.number().withoutDecimals().positive(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export default class get_wikipedia_page_by_id {
|
||||
public async handle(context: HttpContext): Promise<
|
||||
| {
|
||||
__response: ExceptionMessage
|
||||
__status: 404
|
||||
}
|
||||
| {
|
||||
__response: PageWithInternalLinksRaw
|
||||
__status: 200
|
||||
}
|
||||
> {
|
||||
const payload = await context.request.validateUsing(
|
||||
get_wikipedia_page_by_id_validator,
|
||||
)
|
||||
const page = await Page.findOrFail(payload.params.id)
|
||||
await page.load("internalLinks")
|
||||
return context.response.ok(page)
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
.get("/wikipedia/pages/:id", [get_wikipedia_page_by_id])
|
||||
.use(throttle)
|
||||
.openapi({
|
||||
description: "Get a Wikipedia page by ID.",
|
||||
tags: ["wikipedia"],
|
||||
})
|
@ -0,0 +1,93 @@
|
||||
import Page from "#app/models/page.ts"
|
||||
import { PageFactory } from "#database/factories/page_factory.ts"
|
||||
import testUtils from "@adonisjs/core/services/test_utils"
|
||||
import { test } from "@japa/runner"
|
||||
|
||||
test.group("GET /wikipedia/pages", (group) => {
|
||||
group.each.setup(async () => {
|
||||
return await testUtils.db().truncate()
|
||||
})
|
||||
|
||||
test("should succeeds and get the page with the given title", async ({
|
||||
client,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
const page = await PageFactory.create()
|
||||
await PageFactory.createMany(10)
|
||||
|
||||
// Act - When
|
||||
const searchParams = new URLSearchParams({ title: page.title })
|
||||
const response = await client.get(
|
||||
`/wikipedia/pages?${searchParams.toString()}`,
|
||||
)
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(200)
|
||||
response.assertBody([page.toJSON()])
|
||||
})
|
||||
|
||||
test("should succeeds and get the pages with title that starts with the title given and limit", async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
const limit = 4
|
||||
const title = "No"
|
||||
const pagesMatching = await Page.createMany([
|
||||
{ title: "Node.js" },
|
||||
{ title: "North_America" },
|
||||
{ title: "NoSQL" },
|
||||
{ title: "No" },
|
||||
{ title: "Nobel_Prize" },
|
||||
{ title: "Norway" },
|
||||
])
|
||||
await Page.createMany([{ title: "Linux" }, { title: "Abc" }])
|
||||
|
||||
// Act - When
|
||||
const searchParams = new URLSearchParams({
|
||||
title,
|
||||
limit: limit.toString(),
|
||||
})
|
||||
const response = await client.get(
|
||||
`/wikipedia/pages?${searchParams.toString()}`,
|
||||
)
|
||||
const responseBody = response.body()
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(200)
|
||||
response.assertBody(
|
||||
pagesMatching.slice(0, limit).map((page) => {
|
||||
return page.toJSON()
|
||||
}),
|
||||
)
|
||||
assert.equal(responseBody.length, limit)
|
||||
})
|
||||
|
||||
test('should fails when "title" is not provided', async ({ client }) => {
|
||||
// Act - When
|
||||
const response = await client.get("/wikipedia/pages")
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(422)
|
||||
})
|
||||
|
||||
test('should fails when "limit" is too high (more than 100)', async ({
|
||||
client,
|
||||
}) => {
|
||||
// Arrange - Given
|
||||
const title = "No"
|
||||
const limit = 101
|
||||
|
||||
// Act - When
|
||||
const searchParams = new URLSearchParams({
|
||||
title,
|
||||
limit: limit.toString(),
|
||||
})
|
||||
const response = await client.get(
|
||||
`/wikipedia/pages?${searchParams.toString()}`,
|
||||
)
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(422)
|
||||
})
|
||||
})
|
@ -0,0 +1,42 @@
|
||||
import Page, { type PageRaw } from "#app/models/page.ts"
|
||||
import { throttle } from "#start/limiter.ts"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import { sanitizePageTitle } from "@repo/wikipedia"
|
||||
import vine from "@vinejs/vine"
|
||||
|
||||
export const get_wikipedia_pages_validator = vine.compile(
|
||||
vine.object({
|
||||
title: vine
|
||||
.string()
|
||||
.minLength(1)
|
||||
.maxLength(255)
|
||||
.transform((value) => {
|
||||
return sanitizePageTitle(value)
|
||||
}),
|
||||
limit: vine.number().withoutDecimals().range([1, 100]).optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
export default class get_wikipedia_pages {
|
||||
public async handle(context: HttpContext): Promise<{
|
||||
__response: PageRaw[]
|
||||
__status: 200
|
||||
}> {
|
||||
const payload = await context.request.validateUsing(
|
||||
get_wikipedia_pages_validator,
|
||||
)
|
||||
const pages = await Page.query()
|
||||
.whereLike("title", `${payload.title}%`)
|
||||
.limit(payload.limit ?? 5)
|
||||
return context.response.ok(pages)
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
.get("/wikipedia/pages", [get_wikipedia_pages])
|
||||
.use(throttle)
|
||||
.openapi({
|
||||
description: "Search Wikipedia pages by title.",
|
||||
tags: ["wikipedia"],
|
||||
})
|
@ -0,0 +1,226 @@
|
||||
import type { ExceptionMessage } from "#app/exceptions/handler.ts"
|
||||
import Page, { type PageRaw } from "#app/models/page.ts"
|
||||
import { throttle } from "#start/limiter.ts"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import vine from "@vinejs/vine"
|
||||
|
||||
export const get_shortest_paths_validator = vine.compile(
|
||||
vine.object({
|
||||
fromPageId: vine.number().withoutDecimals().positive(),
|
||||
toPageId: vine.number().withoutDecimals().positive(),
|
||||
}),
|
||||
)
|
||||
|
||||
interface get_shortest_paths_response {
|
||||
/**
|
||||
* Object to get page information by their id.
|
||||
* - Key: Page id.
|
||||
* - Value: Page information.
|
||||
*/
|
||||
pages: Record<number, PageRaw>
|
||||
|
||||
/**
|
||||
* Paths between two pages using only internal links.
|
||||
* Each path is an array of page ids.
|
||||
*/
|
||||
paths: number[]
|
||||
}
|
||||
|
||||
const getPathsBetweenPages = async (
|
||||
fromPage: Page,
|
||||
toPage: Page,
|
||||
): Promise<number[]> => {
|
||||
if (fromPage.id === toPage.id) {
|
||||
return [fromPage.id]
|
||||
}
|
||||
|
||||
// A queue of paths from the start page
|
||||
const forwardQueue: Array<[Page, number[]]> = [[fromPage, [fromPage.id]]]
|
||||
// A queue of paths from the end page
|
||||
const backwardQueue: Array<[Page, number[]]> = [[toPage, [toPage.id]]]
|
||||
|
||||
// Sets to track visited pages from both ends
|
||||
const visitedFromStart = new Set<number>([fromPage.id])
|
||||
const visitedFromEnd = new Set<number>([toPage.id])
|
||||
|
||||
// Maps to track paths
|
||||
const forwardPaths = new Map<number, number[]>([[fromPage.id, [fromPage.id]]])
|
||||
const backwardPaths = new Map<number, number[]>([[toPage.id, [toPage.id]]])
|
||||
|
||||
// Helper function to process a queue in one direction
|
||||
const processQueue = async (
|
||||
queue: Array<[Page, number[]]>,
|
||||
visitedThisSide: Set<number>,
|
||||
visitedOtherSide: Set<number>,
|
||||
pathsThisSide: Map<number, number[]>,
|
||||
pathsOtherSide: Map<number, number[]>,
|
||||
): Promise<number[]> => {
|
||||
const [currentPage, currentPath] = queue.shift() as [Page, number[]]
|
||||
await currentPage.load("internalLinks")
|
||||
|
||||
for (const link of currentPage.internalLinks) {
|
||||
if (!visitedThisSide.has(link.id)) {
|
||||
const newPath = [...currentPath, link.id]
|
||||
visitedThisSide.add(link.id)
|
||||
pathsThisSide.set(link.id, newPath)
|
||||
|
||||
// If the other side has visited this page, we've found a meeting point
|
||||
if (visitedOtherSide.has(link.id)) {
|
||||
const pathFromOtherSide = pathsOtherSide.get(link.id) ?? []
|
||||
return [...newPath.slice(0, -1), ...pathFromOtherSide.reverse()]
|
||||
}
|
||||
|
||||
// Otherwise, continue the BFS
|
||||
queue.push([link, newPath])
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
while (forwardQueue.length > 0 && backwardQueue.length > 0) {
|
||||
// Expand the BFS from the start side
|
||||
let result = await processQueue(
|
||||
forwardQueue,
|
||||
visitedFromStart,
|
||||
visitedFromEnd,
|
||||
forwardPaths,
|
||||
backwardPaths,
|
||||
)
|
||||
if (result.length > 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Expand the BFS from the end side
|
||||
result = await processQueue(
|
||||
backwardQueue,
|
||||
visitedFromEnd,
|
||||
visitedFromStart,
|
||||
backwardPaths,
|
||||
forwardPaths,
|
||||
)
|
||||
if (result.length > 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// const getPathsBetweenPages = async (
|
||||
// fromPage: Page,
|
||||
// toPage: Page,
|
||||
// ): Promise<number[][]> => {
|
||||
// const paths: number[][] = []
|
||||
|
||||
// const depthFirstSearch = async (
|
||||
// currentPage: Page,
|
||||
// currentPath: number[],
|
||||
// ): Promise<void> => {
|
||||
// currentPath.push(currentPage.id)
|
||||
// if (currentPage.id === toPage.id) {
|
||||
// paths.push([...currentPath])
|
||||
// } else {
|
||||
// for (const link of currentPage.internalLinks) {
|
||||
// const isAlreadyVisited = currentPath.includes(link.id)
|
||||
// if (!isAlreadyVisited) {
|
||||
// await link.load("internalLinks")
|
||||
// await depthFirstSearch(link, currentPath)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// currentPath.pop()
|
||||
// }
|
||||
|
||||
// await depthFirstSearch(fromPage, [])
|
||||
// return paths
|
||||
// }
|
||||
|
||||
export default class get_shortest_paths {
|
||||
public async handle(context: HttpContext): Promise<
|
||||
| {
|
||||
__response: ExceptionMessage
|
||||
__status: 404
|
||||
}
|
||||
| {
|
||||
__response: ExceptionMessage
|
||||
__status: 500
|
||||
}
|
||||
| {
|
||||
__response: get_shortest_paths_response
|
||||
__status: 200
|
||||
}
|
||||
> {
|
||||
const payload = await context.request.validateUsing(
|
||||
get_shortest_paths_validator,
|
||||
)
|
||||
|
||||
const fromPage = await Page.findOrFail(payload.fromPageId)
|
||||
await fromPage.load("internalLinks")
|
||||
|
||||
const toPage = await Page.findOrFail(payload.toPageId)
|
||||
await toPage.load("internalLinks")
|
||||
|
||||
const isDepth0 = fromPage.id === toPage.id
|
||||
if (isDepth0) {
|
||||
return context.response.ok({
|
||||
pages: {
|
||||
[fromPage.id]: {
|
||||
id: fromPage.id,
|
||||
title: fromPage.title,
|
||||
},
|
||||
},
|
||||
paths: [],
|
||||
})
|
||||
}
|
||||
|
||||
const isDepth1 = fromPage.internalLinks.some((internalLink) => {
|
||||
return internalLink.id === toPage.id
|
||||
})
|
||||
if (isDepth1) {
|
||||
return context.response.ok({
|
||||
pages: {
|
||||
[fromPage.id]: {
|
||||
id: fromPage.id,
|
||||
title: fromPage.title,
|
||||
},
|
||||
[toPage.id]: {
|
||||
id: toPage.id,
|
||||
title: toPage.title,
|
||||
},
|
||||
},
|
||||
paths: [fromPage.id, toPage.id],
|
||||
})
|
||||
}
|
||||
|
||||
const paths = await getPathsBetweenPages(fromPage, toPage)
|
||||
|
||||
return context.response.ok({
|
||||
pages: {
|
||||
[fromPage.id]: {
|
||||
id: fromPage.id,
|
||||
title: fromPage.title,
|
||||
},
|
||||
[toPage.id]: {
|
||||
id: toPage.id,
|
||||
title: toPage.title,
|
||||
},
|
||||
},
|
||||
paths,
|
||||
})
|
||||
|
||||
// return context.response.internalServerError({
|
||||
// message: "Shortest paths can't be determined.",
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
.get("/wikipedia/shortest-paths", [get_shortest_paths])
|
||||
.use(throttle)
|
||||
.openapi({
|
||||
description:
|
||||
"Find the shortest paths between two Wikipedia pages, using only internal links.",
|
||||
tags: ["wikipedia"],
|
||||
})
|
36
apps/api/app/exceptions/handler.ts
Normal file
36
apps/api/app/exceptions/handler.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import { ExceptionHandler } from "@adonisjs/core/http"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
|
||||
export interface ExceptionMessage {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class HttpExceptionHandler extends ExceptionHandler {
|
||||
/**
|
||||
* In debug mode, the exception handler will display verbose errors with pretty printed stack traces.
|
||||
*/
|
||||
protected override debug = !app.inProduction
|
||||
|
||||
/**
|
||||
* The method is used for handling errors and returning response to the client.
|
||||
*/
|
||||
public override async handle(
|
||||
error: unknown,
|
||||
context: HttpContext,
|
||||
): Promise<unknown> {
|
||||
return await super.handle(error, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method is used to report error to the logging service or the third party error monitoring service.
|
||||
*
|
||||
* @note You should not attempt to send a response from this method.
|
||||
*/
|
||||
public override async report(
|
||||
error: unknown,
|
||||
context: HttpContext,
|
||||
): Promise<void> {
|
||||
return await super.report(error, context)
|
||||
}
|
||||
}
|
16
apps/api/app/middleware/app_key_security_middleware.ts
Normal file
16
apps/api/app/middleware/app_key_security_middleware.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { ExceptionMessage } from "#app/exceptions/handler.ts"
|
||||
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.ts"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import type { NextFn } from "@adonisjs/core/types/http"
|
||||
|
||||
export default class AppKeySecurityMiddleware {
|
||||
public async handle(
|
||||
context: HttpContext,
|
||||
next: NextFn,
|
||||
): Promise<unknown | ExceptionMessage> {
|
||||
if (context.request.header(APP_KEY_HEADER_NAME) === APP_KEY) {
|
||||
return next()
|
||||
}
|
||||
return context.response.unauthorized({ message: "Unauthorized access" })
|
||||
}
|
||||
}
|
26
apps/api/app/middleware/auth_middleware.ts
Normal file
26
apps/api/app/middleware/auth_middleware.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { Authenticators } from "@adonisjs/auth/types"
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import type { NextFn } from "@adonisjs/core/types/http"
|
||||
|
||||
/**
|
||||
* Auth middleware is used authenticate HTTP requests and deny access to unauthenticated users.
|
||||
*/
|
||||
export default class AuthMiddleware {
|
||||
/**
|
||||
* The URL to redirect to, when authentication fails.
|
||||
*/
|
||||
redirectTo = "/login"
|
||||
|
||||
public async handle(
|
||||
context: HttpContext,
|
||||
next: NextFn,
|
||||
options: {
|
||||
guards?: Array<keyof Authenticators>
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
await context.auth.authenticateUsing(options.guards, {
|
||||
loginRoute: this.redirectTo,
|
||||
})
|
||||
return next()
|
||||
}
|
||||
}
|
18
apps/api/app/middleware/container_bindings_middleware.ts
Normal file
18
apps/api/app/middleware/container_bindings_middleware.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { HttpContext } from "@adonisjs/core/http"
|
||||
import { Logger } from "@adonisjs/core/logger"
|
||||
import type { NextFn } from "@adonisjs/core/types/http"
|
||||
|
||||
/**
|
||||
* The container bindings middleware binds classes to their request specific value using the container resolver.
|
||||
*
|
||||
* - We bind "HttpContext" class to the "context" object.
|
||||
* - And bind "Logger" class to the "context.logger" object.
|
||||
*/
|
||||
export default class ContainerBindingsMiddleware {
|
||||
public async handle(context: HttpContext, next: NextFn): Promise<void> {
|
||||
context.containerResolver.bindValue(HttpContext, context)
|
||||
context.containerResolver.bindValue(Logger, context.logger)
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
14
apps/api/app/middleware/force_json_response_middleware.ts
Normal file
14
apps/api/app/middleware/force_json_response_middleware.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { HttpContext } from "@adonisjs/core/http"
|
||||
import type { NextFn } from "@adonisjs/core/types/http"
|
||||
|
||||
/**
|
||||
* Updating the "Accept" header to always accept "application/json" response from the server. This will force the internals of the framework like validator errors or auth errors to return a JSON response.
|
||||
*/
|
||||
export default class ForceJsonResponseMiddleware {
|
||||
public async handle({ request }: HttpContext, next: NextFn): Promise<void> {
|
||||
const headers = request.headers()
|
||||
headers.accept = "application/json"
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
42
apps/api/app/models/page.ts
Normal file
42
apps/api/app/models/page.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { BaseModel, column, manyToMany } from "@adonisjs/lucid/orm"
|
||||
import type { ManyToMany } from "@adonisjs/lucid/types/relations"
|
||||
|
||||
export default class Page extends BaseModel {
|
||||
protected tableName = "pages"
|
||||
|
||||
/**
|
||||
* Page id is unique for each page on Wikipedia, can be used to link to the page.
|
||||
* @example `https://${locale}.wikipedia.org/?curid=${pageId}`
|
||||
*/
|
||||
@column({ columnName: "id", serializeAs: "id", isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
/**
|
||||
* Title of the Wikipedia page.
|
||||
*/
|
||||
@column({
|
||||
columnName: "title",
|
||||
serializeAs: "title",
|
||||
})
|
||||
declare title: string
|
||||
|
||||
@manyToMany(
|
||||
() => {
|
||||
return Page
|
||||
},
|
||||
{
|
||||
pivotTable: "internal_links",
|
||||
localKey: "id",
|
||||
relatedKey: "id",
|
||||
pivotForeignKey: "from_page_id",
|
||||
pivotRelatedForeignKey: "to_page_id",
|
||||
serializeAs: "internalLinks",
|
||||
},
|
||||
)
|
||||
declare internalLinks: ManyToMany<typeof Page>
|
||||
}
|
||||
|
||||
export type PageRaw = Pick<Page, "id" | "title">
|
||||
export type PageWithInternalLinksRaw = PageRaw & {
|
||||
internalLinks: PageRaw[]
|
||||
}
|
55
apps/api/app/models/user.ts
Normal file
55
apps/api/app/models/user.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { DbAccessTokensProvider } from "@adonisjs/auth/access_tokens"
|
||||
import { withAuthFinder } from "@adonisjs/auth/mixins/lucid"
|
||||
import { compose } from "@adonisjs/core/helpers"
|
||||
import hash from "@adonisjs/core/services/hash"
|
||||
import { BaseModel, column } from "@adonisjs/lucid/orm"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
const AuthFinder = withAuthFinder(
|
||||
() => {
|
||||
return hash.use("scrypt")
|
||||
},
|
||||
{
|
||||
uids: ["email"],
|
||||
passwordColumnName: "password",
|
||||
},
|
||||
)
|
||||
|
||||
export default class User extends compose(BaseModel, AuthFinder) {
|
||||
protected tableName = "users"
|
||||
|
||||
@column({ columnName: "id", serializeAs: "id", isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
@column({
|
||||
columnName: "full_name",
|
||||
serializeAs: "fullName",
|
||||
})
|
||||
declare fullName: string
|
||||
|
||||
@column({
|
||||
columnName: "email",
|
||||
serializeAs: "email",
|
||||
})
|
||||
declare email: string
|
||||
|
||||
@column({ columnName: "password", serializeAs: null })
|
||||
declare password: string
|
||||
|
||||
@column.dateTime({
|
||||
columnName: "created_at",
|
||||
serializeAs: "createdAt",
|
||||
autoCreate: true,
|
||||
})
|
||||
declare createdAt: DateTime
|
||||
|
||||
@column.dateTime({
|
||||
columnName: "updated_at",
|
||||
serializeAs: "updatedAt",
|
||||
autoCreate: true,
|
||||
autoUpdate: true,
|
||||
})
|
||||
declare updatedAt: DateTime
|
||||
|
||||
static accessTokens = DbAccessTokensProvider.forModel(User)
|
||||
}
|
48
apps/api/bin/console.ts
Executable file
48
apps/api/bin/console.ts
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env -S node --import=tsx
|
||||
|
||||
/**
|
||||
* Ace entry point
|
||||
*
|
||||
* Entrypoint for booting the AdonisJS command-line framework and executing commands.
|
||||
* Commands do not boot the application, unless the currently running command has `options.startApp` flag set to true.
|
||||
*/
|
||||
|
||||
import { Ignitor, prettyPrintError } from "@adonisjs/core"
|
||||
import "reflect-metadata"
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL("../", import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the application.
|
||||
*/
|
||||
const IMPORTER = async (filePath: string): Promise<unknown> => {
|
||||
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
||||
return await import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return await import(filePath)
|
||||
}
|
||||
|
||||
const ignitor = new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
|
||||
try {
|
||||
await ignitor
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import("#start/env.ts")
|
||||
})
|
||||
app.listen("SIGTERM", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
app.listenIf(app.managedByPm2, "SIGINT", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
})
|
||||
.ace()
|
||||
.handle(process.argv.splice(2))
|
||||
} catch (error) {
|
||||
process.exitCode = 1
|
||||
await prettyPrintError(error)
|
||||
}
|
47
apps/api/bin/server.ts
Executable file
47
apps/api/bin/server.ts
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env -S node --import=tsx
|
||||
|
||||
/**
|
||||
* HTTP server entrypoint
|
||||
*
|
||||
* Entrypoint for starting the AdonisJS HTTP server.
|
||||
*/
|
||||
|
||||
import { Ignitor, prettyPrintError } from "@adonisjs/core"
|
||||
import "reflect-metadata"
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve paths to file and directories for scaffolding commands.
|
||||
*/
|
||||
const APP_ROOT = new URL("../", import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the application.
|
||||
*/
|
||||
const IMPORTER = async (filePath: string): Promise<unknown> => {
|
||||
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
||||
return await import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return await import(filePath)
|
||||
}
|
||||
|
||||
const ignitor = new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
|
||||
try {
|
||||
await ignitor
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import("#start/env.ts")
|
||||
})
|
||||
app.listen("SIGTERM", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
app.listenIf(app.managedByPm2, "SIGINT", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
})
|
||||
.httpServer()
|
||||
.start()
|
||||
} catch (error) {
|
||||
process.exitCode = 1
|
||||
await prettyPrintError(error)
|
||||
}
|
72
apps/api/bin/test.ts
Executable file
72
apps/api/bin/test.ts
Executable file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env -S node --import=tsx
|
||||
|
||||
/**
|
||||
* Test runner entrypoint
|
||||
*
|
||||
* Entrypoint for running tests using Japa.
|
||||
*/
|
||||
|
||||
process.env["NODE_ENV"] = "test"
|
||||
process.env["PORT"] = "3333"
|
||||
process.env["LIMITER_STORE"] = "memory"
|
||||
process.env["LOG_LEVEL"] = "error"
|
||||
|
||||
import { Ignitor, prettyPrintError } from "@adonisjs/core"
|
||||
import { configure, processCLIArgs, run } from "@japa/runner"
|
||||
import "reflect-metadata"
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL("../", import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the application.
|
||||
*/
|
||||
const IMPORTER = async (filePath: string): Promise<unknown> => {
|
||||
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
||||
return await import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return await import(filePath)
|
||||
}
|
||||
|
||||
const ignitor = new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
|
||||
try {
|
||||
await ignitor
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import("#start/env.ts")
|
||||
})
|
||||
app.listen("SIGTERM", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
app.listenIf(app.managedByPm2, "SIGINT", async () => {
|
||||
return await app.terminate()
|
||||
})
|
||||
})
|
||||
.testRunner()
|
||||
.configure(async (app) => {
|
||||
const { runnerHooks, ...config } = await import("#tests/bootstrap.ts")
|
||||
|
||||
processCLIArgs(process.argv.splice(2))
|
||||
configure({
|
||||
...app.rcFile.tests,
|
||||
...config,
|
||||
...{
|
||||
setup: runnerHooks.setup,
|
||||
teardown: runnerHooks.teardown.concat([
|
||||
async () => {
|
||||
return await app.terminate()
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
.run(async () => {
|
||||
return await run()
|
||||
})
|
||||
} catch (error) {
|
||||
process.exitCode = 1
|
||||
await prettyPrintError(error)
|
||||
}
|
39
apps/api/config/app.ts
Normal file
39
apps/api/config/app.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import env from "#start/env.ts"
|
||||
import { Secret } from "@adonisjs/core/helpers"
|
||||
import { defineConfig } from "@adonisjs/core/http"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
|
||||
/**
|
||||
* The app key is used for encrypting cookies, generating signed URLs, and by the "encryption" module.
|
||||
*
|
||||
* The encryption module will fail to decrypt data if the key is lost or changed.
|
||||
* Therefore it is recommended to keep the app key secure.
|
||||
*/
|
||||
export const APP_KEY_HEADER_NAME = "x-app-key"
|
||||
export const APP_KEY = env.get("APP_KEY")
|
||||
export const appKey = new Secret(APP_KEY)
|
||||
|
||||
/**
|
||||
* The configuration settings used by the HTTP server
|
||||
*/
|
||||
export const http = defineConfig({
|
||||
generateRequestId: true,
|
||||
allowMethodSpoofing: false,
|
||||
|
||||
/**
|
||||
* Enabling async local storage will let you access HTTP context from anywhere inside your application.
|
||||
*/
|
||||
useAsyncLocalStorage: false,
|
||||
|
||||
/**
|
||||
* Manage cookies configuration. The settings for the session id cookie are defined inside the "config/session.ts" file.
|
||||
*/
|
||||
cookie: {
|
||||
domain: "",
|
||||
path: "/",
|
||||
maxAge: "2h",
|
||||
httpOnly: true,
|
||||
secure: app.inProduction,
|
||||
sameSite: "lax",
|
||||
},
|
||||
})
|
29
apps/api/config/auth.ts
Normal file
29
apps/api/config/auth.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { defineConfig } from "@adonisjs/auth"
|
||||
import { tokensGuard, tokensUserProvider } from "@adonisjs/auth/access_tokens"
|
||||
import type { Authenticators, InferAuthEvents } from "@adonisjs/auth/types"
|
||||
|
||||
const authConfig = defineConfig({
|
||||
default: "api",
|
||||
guards: {
|
||||
api: tokensGuard({
|
||||
provider: tokensUserProvider({
|
||||
tokens: "accessTokens",
|
||||
model: async () => {
|
||||
return await import("#app/models/user.ts")
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default authConfig
|
||||
|
||||
/**
|
||||
* Inferring types from the configured auth guards.
|
||||
*/
|
||||
declare module "@adonisjs/auth/types" {
|
||||
interface Authenticators extends InferAuthenticators<typeof authConfig> {}
|
||||
}
|
||||
declare module "@adonisjs/core/types" {
|
||||
interface EventsList extends InferAuthEvents<Authenticators> {}
|
||||
}
|
50
apps/api/config/bodyparser.ts
Normal file
50
apps/api/config/bodyparser.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { defineConfig } from "@adonisjs/core/bodyparser"
|
||||
|
||||
const bodyParserConfig = defineConfig({
|
||||
/**
|
||||
* The bodyparser middleware will parse the request body for the following HTTP methods.
|
||||
*/
|
||||
allowedMethods: ["POST", "PUT", "PATCH", "DELETE"],
|
||||
|
||||
/**
|
||||
* Config for the "application/x-www-form-urlencoded" content-type parser.
|
||||
*/
|
||||
form: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: ["application/x-www-form-urlencoded"],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the JSON parser.
|
||||
*/
|
||||
json: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: [
|
||||
"application/json",
|
||||
"application/json-patch+json",
|
||||
"application/vnd.api+json",
|
||||
"application/csp-report",
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the "multipart/form-data" content-type parser.
|
||||
* File uploads are handled by the multipart parser.
|
||||
*/
|
||||
multipart: {
|
||||
/**
|
||||
* Enabling auto process allows bodyparser middleware to move all uploaded files inside the tmp folder of your operating system.
|
||||
*/
|
||||
autoProcess: true,
|
||||
convertEmptyStringsToNull: true,
|
||||
processManually: [],
|
||||
|
||||
/**
|
||||
* Maximum limit of data to parse including all files and fields.
|
||||
*/
|
||||
limit: "20mb",
|
||||
types: ["multipart/form-data"],
|
||||
},
|
||||
})
|
||||
|
||||
export default bodyParserConfig
|
18
apps/api/config/cors.ts
Normal file
18
apps/api/config/cors.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "@adonisjs/cors"
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the CORS policy. The following options are documented on the official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/security/cors
|
||||
*/
|
||||
const corsConfig = defineConfig({
|
||||
enabled: true,
|
||||
origin: true,
|
||||
methods: ["GET", "HEAD", "POST", "PUT", "DELETE"],
|
||||
headers: true,
|
||||
exposeHeaders: [],
|
||||
credentials: true,
|
||||
maxAge: 90,
|
||||
})
|
||||
|
||||
export default corsConfig
|
38
apps/api/config/database.ts
Normal file
38
apps/api/config/database.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import env from "#start/env.ts"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
import { defineConfig } from "@adonisjs/lucid"
|
||||
|
||||
const databaseConfig = defineConfig({
|
||||
prettyPrintDebugQueries: !app.inProduction,
|
||||
connection: app.inTest ? "sqlite" : "postgres",
|
||||
connections: {
|
||||
postgres: {
|
||||
debug: env.get("DATABASE_DEBUG"),
|
||||
client: "pg",
|
||||
connection: {
|
||||
host: env.get("DATABASE_HOST"),
|
||||
port: env.get("DATABASE_PORT"),
|
||||
user: env.get("DATABASE_USER"),
|
||||
password: env.get("DATABASE_PASSWORD"),
|
||||
database: env.get("DATABASE_NAME"),
|
||||
},
|
||||
migrations: {
|
||||
naturalSort: true,
|
||||
paths: ["database/migrations"],
|
||||
},
|
||||
},
|
||||
sqlite: {
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: ":memory:",
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
migrations: {
|
||||
naturalSort: true,
|
||||
paths: ["database/migrations"],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default databaseConfig
|
23
apps/api/config/hash.ts
Normal file
23
apps/api/config/hash.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { defineConfig, drivers } from "@adonisjs/core/hash"
|
||||
|
||||
const hashConfig = defineConfig({
|
||||
default: "scrypt",
|
||||
|
||||
list: {
|
||||
scrypt: drivers.scrypt({
|
||||
cost: 16384,
|
||||
blockSize: 8,
|
||||
parallelization: 1,
|
||||
maxMemory: 33554432,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default hashConfig
|
||||
|
||||
/**
|
||||
* Inferring types for the list of hashers you have configured in your application.
|
||||
*/
|
||||
declare module "@adonisjs/core/types" {
|
||||
export interface HashersList extends InferHashers<typeof hashConfig> {}
|
||||
}
|
26
apps/api/config/limiter.ts
Normal file
26
apps/api/config/limiter.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import env from "#start/env.ts"
|
||||
import { defineConfig, stores } from "@adonisjs/limiter"
|
||||
|
||||
const limiterConfig = defineConfig({
|
||||
default: env.get("LIMITER_STORE"),
|
||||
stores: {
|
||||
/**
|
||||
* Database store to save rate limiting data inside a database.
|
||||
*/
|
||||
database: stores.database({
|
||||
tableName: "rate_limits",
|
||||
clearExpiredByTimeout: true,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Memory store could be used during testing.
|
||||
*/
|
||||
memory: stores.memory({}),
|
||||
},
|
||||
})
|
||||
|
||||
export default limiterConfig
|
||||
|
||||
declare module "@adonisjs/limiter/types" {
|
||||
export interface LimitersList extends InferLimiters<typeof limiterConfig> {}
|
||||
}
|
34
apps/api/config/logger.ts
Normal file
34
apps/api/config/logger.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import env from "#start/env.ts"
|
||||
import { defineConfig, targets } from "@adonisjs/core/logger"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
|
||||
const loggerConfig = defineConfig({
|
||||
default: "app",
|
||||
|
||||
/**
|
||||
* The loggers object can be used to define multiple loggers.
|
||||
* By default, we configure only one logger (named "app").
|
||||
*/
|
||||
loggers: {
|
||||
app: {
|
||||
enabled: true,
|
||||
name: env.get("APP_NAME"),
|
||||
level: env.get("LOG_LEVEL"),
|
||||
transport: {
|
||||
targets: targets()
|
||||
.pushIf(!app.inProduction, targets.pretty())
|
||||
.pushIf(app.inProduction, targets.file({ destination: 1 }))
|
||||
.toArray(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default loggerConfig
|
||||
|
||||
/**
|
||||
* Inferring types for the list of loggers you have configured in your application.
|
||||
*/
|
||||
declare module "@adonisjs/core/types" {
|
||||
export interface LoggersList extends InferLoggers<typeof loggerConfig> {}
|
||||
}
|
21
apps/api/config/tuyau.ts
Normal file
21
apps/api/config/tuyau.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import env from "#start/env.ts"
|
||||
import { VERSION } from "@repo/utils/constants"
|
||||
import { defineConfig } from "@tuyau/core"
|
||||
|
||||
const tuyauConfig = defineConfig({
|
||||
codegen: {},
|
||||
openapi: {
|
||||
provider: "scalar",
|
||||
buildSpecPath: ".adonisjs/openapi.yaml",
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Wikipedia Game Solver API",
|
||||
version: VERSION,
|
||||
},
|
||||
tags: [{ name: "health" }, { name: "users" }, { name: "wikipedia" }],
|
||||
servers: [{ url: env.get("API_URL") }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default tuyauConfig
|
14
apps/api/database/factories/page_factory.ts
Normal file
14
apps/api/database/factories/page_factory.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import Page from "#app/models/page.ts"
|
||||
import factory from "@adonisjs/lucid/factories"
|
||||
import { sanitizePageTitle } from "@repo/wikipedia"
|
||||
|
||||
export const PageFactory = factory
|
||||
.define(Page, async ({ faker }) => {
|
||||
return {
|
||||
title: sanitizePageTitle(faker.commerce.productName()),
|
||||
}
|
||||
})
|
||||
.relation("internalLinks", () => {
|
||||
return []
|
||||
})
|
||||
.build()
|
@ -0,0 +1,21 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreateUsersTable extends BaseSchema {
|
||||
protected tableName = "users"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments("id").notNullable()
|
||||
table.string("full_name").notNullable()
|
||||
table.string("email", 254).notNullable().unique()
|
||||
table.string("password").notNullable()
|
||||
|
||||
table.timestamp("created_at").notNullable()
|
||||
table.timestamp("updated_at").notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreateAccessTokensTable extends BaseSchema {
|
||||
protected tableName = "auth_access_tokens"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments("id")
|
||||
table
|
||||
.integer("tokenable_id")
|
||||
.notNullable()
|
||||
.unsigned()
|
||||
.references("users.id")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE")
|
||||
|
||||
table.string("type").notNullable()
|
||||
table.string("name").nullable()
|
||||
table.string("hash").notNullable()
|
||||
table.text("abilities").notNullable()
|
||||
table.timestamp("created_at")
|
||||
table.timestamp("updated_at")
|
||||
table.timestamp("last_used_at").nullable()
|
||||
table.timestamp("expires_at").nullable()
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreatePagesTable extends BaseSchema {
|
||||
protected tableName = "pages"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments("id").notNullable()
|
||||
table.string("title", 255).notNullable().unique()
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreateInternalLinksTable extends BaseSchema {
|
||||
protected tableName = "internal_links"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.primary(["from_page_id", "to_page_id"])
|
||||
table
|
||||
.integer("from_page_id")
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references("pages.id")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE")
|
||||
table
|
||||
.integer("to_page_id")
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references("pages.id")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE")
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { BaseSchema } from "@adonisjs/lucid/schema"
|
||||
|
||||
export default class CreateRateLimitsTable extends BaseSchema {
|
||||
protected tableName = "rate_limits"
|
||||
|
||||
public override async up(): Promise<void> {
|
||||
void this.schema.createTable(this.tableName, (table) => {
|
||||
table.string("key", 255).notNullable().primary()
|
||||
table.integer("points", 9).notNullable().defaultTo(0)
|
||||
table.bigint("expire").unsigned()
|
||||
})
|
||||
}
|
||||
|
||||
public override async down(): Promise<void> {
|
||||
void this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
59
apps/api/package.json
Normal file
59
apps/api/package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@repo/api",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#*": "./*"
|
||||
},
|
||||
"exports": {
|
||||
".": "./.adonisjs/api.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --import=tsx ./bin/server.ts",
|
||||
"dev": "node --import=tsx --watch --watch-preserve-output ./bin/server.ts",
|
||||
"ace": "node --import=tsx ./bin/console.ts",
|
||||
"tuyau": "node --run ace -- tuyau:generate && node --run ace -- tuyau:generate:openapi --destination=\".adonisjs/openapi.yaml\"",
|
||||
"build": "node --run tuyau",
|
||||
"database:migrate": "node --run ace -- migration:run",
|
||||
"test": "c8 node --import=tsx ./bin/test.ts",
|
||||
"test-shortest-paths": "node --import=tsx ./shortest-paths-tests.ts",
|
||||
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives",
|
||||
"lint:typescript": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "catalog:",
|
||||
"@adonisjs/core": "catalog:",
|
||||
"@adonisjs/cors": "catalog:",
|
||||
"@adonisjs/lucid": "catalog:",
|
||||
"@adonisjs/limiter": "catalog:",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@repo/wikipedia": "workspace:*",
|
||||
"@tuyau/core": "catalog:",
|
||||
"@tuyau/utils": "catalog:",
|
||||
"@tuyau/openapi": "catalog:",
|
||||
"@vinejs/vine": "catalog:",
|
||||
"luxon": "catalog:",
|
||||
"pg": "catalog:",
|
||||
"reflect-metadata": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"pino-pretty": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "catalog:",
|
||||
"@japa/api-client": "catalog:",
|
||||
"@japa/assert": "catalog:",
|
||||
"@japa/plugin-adonisjs": "catalog:",
|
||||
"@japa/runner": "catalog:",
|
||||
"@repo/config-typescript": "workspace:*",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@total-typescript/ts-reset": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"better-sqlite3": "catalog:",
|
||||
"c8": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"openapi-types": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
170
apps/api/shortest-paths-tests.ts
Normal file
170
apps/api/shortest-paths-tests.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { type PageWithInternalLinksRaw } from "#app/models/page.ts"
|
||||
|
||||
const DATA: { [key: number]: PageWithInternalLinksRaw } = {
|
||||
0: {
|
||||
id: 0,
|
||||
title: "Page 0",
|
||||
internalLinks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Page 1",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Page 4",
|
||||
},
|
||||
],
|
||||
},
|
||||
1: {
|
||||
id: 1,
|
||||
title: "Page 1",
|
||||
internalLinks: [
|
||||
{
|
||||
id: 2,
|
||||
title: "Page 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: "Page 2",
|
||||
internalLinks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Page 1",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Page 3",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Page 4",
|
||||
},
|
||||
],
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
title: "Page 3",
|
||||
internalLinks: [],
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
title: "Page 4",
|
||||
internalLinks: [
|
||||
{
|
||||
id: 2,
|
||||
title: "Page 2",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Page 3",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// PILE (stack): LIFO: .pop()
|
||||
// FILE (queue): FIFO: .shift()
|
||||
// parcours en profondeur, ou DFS, pour Depth-First Search
|
||||
// get all possible paths from 0 to 3
|
||||
// [[0, 1, 2, 3], [0, 1, 2, 4, 3], [0, 4, 2, 3], [0, 4, 3]]
|
||||
|
||||
// console.log(DATA)
|
||||
|
||||
const getPathsBetweenPages = async (
|
||||
fromPage: PageWithInternalLinksRaw,
|
||||
toPage: PageWithInternalLinksRaw,
|
||||
getPageById: (id: number) => Promise<PageWithInternalLinksRaw>,
|
||||
): Promise<number[][]> => {
|
||||
const paths: number[][] = []
|
||||
|
||||
const depthFirstSearch = async (
|
||||
currentPage: PageWithInternalLinksRaw,
|
||||
currentPath: number[],
|
||||
): Promise<void> => {
|
||||
currentPath.push(currentPage.id)
|
||||
if (currentPage.id === toPage.id) {
|
||||
paths.push([...currentPath])
|
||||
} else {
|
||||
for (const link of currentPage.internalLinks) {
|
||||
const isAlreadyVisited = currentPath.includes(link.id)
|
||||
if (!isAlreadyVisited) {
|
||||
await depthFirstSearch(await getPageById(link.id), currentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
currentPath.pop()
|
||||
}
|
||||
|
||||
await depthFirstSearch(fromPage, [])
|
||||
return paths
|
||||
}
|
||||
|
||||
const getShortestPathsBetweenPages = async (
|
||||
fromPage: PageWithInternalLinksRaw,
|
||||
toPage: PageWithInternalLinksRaw,
|
||||
getPageById: (id: number) => Promise<PageWithInternalLinksRaw>,
|
||||
): Promise<number[][]> => {
|
||||
const shortestPaths: number[][] = []
|
||||
const queue: Array<{ page: PageWithInternalLinksRaw; path: number[] }> = [
|
||||
{ page: fromPage, path: [fromPage.id] },
|
||||
]
|
||||
let shortestLength: number | null = null
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { page, path } = queue.shift() as {
|
||||
page: PageWithInternalLinksRaw
|
||||
path: number[]
|
||||
}
|
||||
|
||||
if (page.id === toPage.id) {
|
||||
// If we reached the destination, check the path length
|
||||
if (shortestLength === null || path.length <= shortestLength) {
|
||||
if (shortestLength === null) {
|
||||
shortestLength = path.length
|
||||
}
|
||||
if (path.length === shortestLength) {
|
||||
shortestPaths.push(path)
|
||||
}
|
||||
}
|
||||
// If we found a shorter path, discard previously found paths
|
||||
else if (path.length < shortestLength) {
|
||||
shortestPaths.length = 0
|
||||
shortestPaths.push(path)
|
||||
shortestLength = path.length
|
||||
}
|
||||
} else {
|
||||
for (const link of page.internalLinks) {
|
||||
if (!path.includes(link.id)) {
|
||||
queue.push({
|
||||
page: await getPageById(link.id),
|
||||
path: [...path, link.id],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shortestPaths
|
||||
}
|
||||
|
||||
console.log(
|
||||
await getPathsBetweenPages(
|
||||
DATA[0] as PageWithInternalLinksRaw,
|
||||
DATA[3] as PageWithInternalLinksRaw,
|
||||
async (id) => {
|
||||
return DATA[id] as PageWithInternalLinksRaw
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
console.log(
|
||||
await getShortestPathsBetweenPages(
|
||||
DATA[0] as PageWithInternalLinksRaw,
|
||||
DATA[3] as PageWithInternalLinksRaw,
|
||||
async (id) => {
|
||||
return DATA[id] as PageWithInternalLinksRaw
|
||||
},
|
||||
),
|
||||
)
|
3
apps/api/start/database.ts
Normal file
3
apps/api/start/database.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { BaseModel, CamelCaseNamingStrategy } from "@adonisjs/lucid/orm"
|
||||
|
||||
BaseModel.namingStrategy = new CamelCaseNamingStrategy()
|
37
apps/api/start/env.ts
Normal file
37
apps/api/start/env.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Environment variables service
|
||||
*/
|
||||
|
||||
import { Env } from "@adonisjs/core/env"
|
||||
|
||||
export default await Env.create(new URL("..", import.meta.url), {
|
||||
TZ: Env.schema.string(),
|
||||
PORT: Env.schema.number(),
|
||||
HOST: Env.schema.string({ format: "host" }),
|
||||
API_URL: Env.schema.string({ format: "url" }),
|
||||
LOG_LEVEL: Env.schema.enum([
|
||||
"fatal",
|
||||
"error",
|
||||
"warn",
|
||||
"info",
|
||||
"debug",
|
||||
"trace",
|
||||
] as const),
|
||||
APP_KEY: Env.schema.string(),
|
||||
NODE_ENV: Env.schema.enum(["development", "production", "test"] as const),
|
||||
|
||||
/**
|
||||
* Variables for configuring database connection.
|
||||
*/
|
||||
DATABASE_HOST: Env.schema.string({ format: "host" }),
|
||||
DATABASE_PORT: Env.schema.number(),
|
||||
DATABASE_USER: Env.schema.string(),
|
||||
DATABASE_PASSWORD: Env.schema.string(),
|
||||
DATABASE_NAME: Env.schema.string(),
|
||||
DATABASE_DEBUG: Env.schema.boolean(),
|
||||
|
||||
/**
|
||||
* Variables for configuring the limiter package.
|
||||
*/
|
||||
LIMITER_STORE: Env.schema.enum(["database", "memory"] as const),
|
||||
})
|
13
apps/api/start/health.ts
Normal file
13
apps/api/start/health.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {
|
||||
DiskSpaceCheck,
|
||||
HealthChecks,
|
||||
MemoryHeapCheck,
|
||||
} from "@adonisjs/core/health"
|
||||
import { DbCheck } from "@adonisjs/lucid/database"
|
||||
import db from "@adonisjs/lucid/services/db"
|
||||
|
||||
export const healthChecks = new HealthChecks().register([
|
||||
new DiskSpaceCheck(),
|
||||
new MemoryHeapCheck(),
|
||||
new DbCheck(db.connection()),
|
||||
])
|
54
apps/api/start/kernel.ts
Normal file
54
apps/api/start/kernel.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* HTTP kernel file
|
||||
*
|
||||
* The HTTP kernel file is used to register the middleware with the server or the router.
|
||||
*/
|
||||
|
||||
import router from "@adonisjs/core/services/router"
|
||||
import server from "@adonisjs/core/services/server"
|
||||
|
||||
/**
|
||||
* The error handler is used to convert an exception to a HTTP response.
|
||||
*/
|
||||
server.errorHandler(async () => {
|
||||
return await import("#app/exceptions/handler.ts")
|
||||
})
|
||||
|
||||
/**
|
||||
* The server middleware stack runs middleware on all the HTTP requests, even if there is no route registered for the requested URL.
|
||||
*/
|
||||
server.use([
|
||||
async () => {
|
||||
return await import("#app/middleware/container_bindings_middleware.ts")
|
||||
},
|
||||
async () => {
|
||||
return await import("#app/middleware/force_json_response_middleware.ts")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/cors/cors_middleware")
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* The router middleware stack runs middleware on all the HTTP requests with a registered route.
|
||||
*/
|
||||
router.use([
|
||||
async () => {
|
||||
return await import("@adonisjs/core/bodyparser_middleware")
|
||||
},
|
||||
async () => {
|
||||
return await import("@adonisjs/auth/initialize_auth_middleware")
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* Named middleware collection must be explicitly assigned to the routes or the routes group.
|
||||
*/
|
||||
export const middleware = router.named({
|
||||
appKeySecurity: async () => {
|
||||
return await import("#app/middleware/app_key_security_middleware.ts")
|
||||
},
|
||||
auth: async () => {
|
||||
return await import("#app/middleware/auth_middleware.ts")
|
||||
},
|
||||
})
|
17
apps/api/start/limiter.ts
Normal file
17
apps/api/start/limiter.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Define HTTP limiters
|
||||
*
|
||||
* The "limiter.define" method creates an HTTP middleware to apply rate limits on a route or a group of routes. Feel free to define as many throttle middleware as needed.
|
||||
*/
|
||||
|
||||
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.ts"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
import type { HttpLimiter } from "@adonisjs/limiter"
|
||||
import limiter from "@adonisjs/limiter/services/main"
|
||||
|
||||
export const throttle = limiter.define("global", (context) => {
|
||||
if (app.inTest || context.request.header(APP_KEY_HEADER_NAME) === APP_KEY) {
|
||||
return limiter.noLimit()
|
||||
}
|
||||
return limiter.allowRequests(120).every("1 minute") as HttpLimiter<any>
|
||||
})
|
30
apps/api/start/routes.ts
Normal file
30
apps/api/start/routes.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Routes file
|
||||
*
|
||||
* The routes file is used for defining the HTTP routes.
|
||||
* Routes are loaded automatically from "app/controllers".
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const CONTROLLERS_DIRECTORY_URL = new URL(
|
||||
"../app/controllers/",
|
||||
import.meta.url,
|
||||
)
|
||||
|
||||
const controllers = (
|
||||
await fs.promises.readdir(CONTROLLERS_DIRECTORY_URL, {
|
||||
recursive: true,
|
||||
withFileTypes: true,
|
||||
})
|
||||
).filter((item) => {
|
||||
return item.isFile() && !item.name.includes(".test.")
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
controllers.map(async (controller) => {
|
||||
const controllerPath = path.join(controller.parentPath, controller.name)
|
||||
await import(controllerPath)
|
||||
}),
|
||||
)
|
45
apps/api/tests/bootstrap.ts
Normal file
45
apps/api/tests/bootstrap.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import app from "@adonisjs/core/services/app"
|
||||
import testUtils from "@adonisjs/core/services/test_utils"
|
||||
import { apiClient } from "@japa/api-client"
|
||||
import { assert } from "@japa/assert"
|
||||
import { pluginAdonisJS } from "@japa/plugin-adonisjs"
|
||||
import type { Config } from "@japa/runner/types"
|
||||
|
||||
/**
|
||||
* This file is imported by the "bin/test.ts" entrypoint file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configure Japa plugins in the plugins array.
|
||||
* Learn more - https://japa.dev/docs/runner-config#plugins-optional
|
||||
*/
|
||||
export const plugins: Config["plugins"] = [
|
||||
assert(),
|
||||
apiClient(),
|
||||
pluginAdonisJS(app),
|
||||
]
|
||||
|
||||
/**
|
||||
* Configure lifecycle function to run before and after all the tests.
|
||||
*
|
||||
* The setup functions are executed before all the tests.
|
||||
* The teardown functions are executer after all the tests.
|
||||
*/
|
||||
export const runnerHooks: Required<Pick<Config, "setup" | "teardown">> = {
|
||||
setup: [
|
||||
async () => {
|
||||
return await testUtils.db().truncate()
|
||||
},
|
||||
],
|
||||
teardown: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure suites by tapping into the test suite instance.
|
||||
* Learn more - https://japa.dev/docs/test-suites#lifecycle-hooks
|
||||
*/
|
||||
export const configureSuite: Config["configureSuite"] = (suite) => {
|
||||
return suite.setup(async () => {
|
||||
return await testUtils.httpServer().start()
|
||||
})
|
||||
}
|
7
apps/api/tsconfig.json
Normal file
7
apps/api/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@repo/config-typescript/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"types": ["@total-typescript/ts-reset", "@types/node"]
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/cli",
|
||||
"version": "1.0.0-staging.1",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
@ -11,13 +11,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --import=tsx ./src/index.ts",
|
||||
"dev": "node --import=tsx --watch --watch-preserve-output ./src/index.ts",
|
||||
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||
"lint:typescript": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/wikipedia-game-solver": "workspace:*",
|
||||
"@repo/constants": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"tsx": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,11 +1,62 @@
|
||||
#!/usr/bin/env -S node --import=tsx
|
||||
|
||||
import { add } from "#abc/def/add.js"
|
||||
import type { WikipediaPagesInternalLinks } from "@repo/wikipedia-game-solver/wikipedia-api"
|
||||
import { getWikipediaPageInternalLinks } from "@repo/wikipedia-game-solver/wikipedia-api"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
import { VERSION } from "@repo/constants"
|
||||
import { sum } from "@repo/wikipedia-game-solver/wikipedia-api"
|
||||
const localeWikipedia = "en"
|
||||
const cachePath = path.join(process.cwd(), "cache.json")
|
||||
|
||||
console.log("Hello, world!")
|
||||
console.log(sum(1, 2))
|
||||
console.log(add(2, 3))
|
||||
console.log(`v${VERSION}`)
|
||||
const fromPageInput = "New York City"
|
||||
// const fromPageInput = "Linux"
|
||||
// const toPageInput = "Node.js"
|
||||
// console.log({
|
||||
// fromPageInput,
|
||||
// toPageInput,
|
||||
// })
|
||||
// const [fromPageWikipediaLinks, toPageWikipediaLinks] = await Promise.all([
|
||||
// getWikipediaPageInternalLinks({
|
||||
// title: fromPageInput,
|
||||
// locale: localeWikipedia,
|
||||
// }),
|
||||
// getWikipediaPageInternalLinks({
|
||||
// title: toPageInput,
|
||||
// locale: localeWikipedia,
|
||||
// }),
|
||||
// ])
|
||||
// console.log({
|
||||
// fromPageWikipediaLinks,
|
||||
// toPageWikipediaLinks,
|
||||
// })
|
||||
// const data = {
|
||||
// [fromPageWikipediaLinks.title]: fromPageWikipediaLinks,
|
||||
// [toPageWikipediaLinks.title]: toPageWikipediaLinks,
|
||||
// }
|
||||
|
||||
const data = JSON.parse(
|
||||
await fs.promises.readFile(cachePath, { encoding: "utf-8" }),
|
||||
) as WikipediaPagesInternalLinks
|
||||
|
||||
// let maxLinks = { max: 0, title: "" }
|
||||
// for (const [title, page] of Object.entries(data)) {
|
||||
// if (page.links.length > maxLinks.max) {
|
||||
// maxLinks = { max: page.links.length, title }
|
||||
// }
|
||||
// }
|
||||
// console.log(maxLinks)
|
||||
|
||||
const pageLinks = (data[fromPageInput]?.links ?? []).slice(0, 1100)
|
||||
for (const pageLink of pageLinks) {
|
||||
if (pageLink in data) {
|
||||
continue
|
||||
}
|
||||
console.log("Fetching", pageLink)
|
||||
data[pageLink] = await getWikipediaPageInternalLinks({
|
||||
title: pageLink,
|
||||
locale: localeWikipedia,
|
||||
})
|
||||
}
|
||||
await fs.promises.writeFile(cachePath, JSON.stringify(data, null, 2), {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
|
9
apps/cli/src/main.ts
Executable file
9
apps/cli/src/main.ts
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env -S node --import=tsx
|
||||
|
||||
import { add } from "#abc/def/add.ts"
|
||||
|
||||
import { VERSION } from "@repo/utils/constants"
|
||||
|
||||
console.log("Hello, world!")
|
||||
console.log(add(2, 3))
|
||||
console.log(`v${VERSION}`)
|
@ -1,13 +1,7 @@
|
||||
{
|
||||
"extends": "@repo/config-typescript/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ESNext"],
|
||||
"types": ["@total-typescript/ts-reset", "@types/node"],
|
||||
|
||||
"noEmit": true
|
||||
"types": ["@total-typescript/ts-reset", "@types/node"]
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import "@repo/config-tailwind/styles.css"
|
||||
import { defaultTranslationValues } from "@repo/i18n/config"
|
||||
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
|
||||
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
|
||||
import type { Locale } from "@repo/utils/constants"
|
||||
import type { Preview } from "@storybook/react"
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
import { ThemeProvider as NextThemeProvider } from "next-themes"
|
||||
import React from "react"
|
||||
|
||||
const preview: Preview = {
|
||||
@ -13,7 +14,7 @@ const preview: Preview = {
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
order: ["Design System", "User Interface", "Feature"],
|
||||
order: ["Design System", "Layout", "Errors"],
|
||||
},
|
||||
},
|
||||
backgrounds: { disable: true },
|
||||
@ -32,16 +33,18 @@ const preview: Preview = {
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const locale = "en-US" satisfies Locale
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<NextThemeProvider enableColorScheme={false}>
|
||||
<NextIntlClientProvider
|
||||
messages={i18nMessagesEnglish}
|
||||
locale="en"
|
||||
locale={locale}
|
||||
defaultTranslationValues={defaultTranslationValues}
|
||||
>
|
||||
<Story />
|
||||
</NextIntlClientProvider>
|
||||
</ThemeProvider>
|
||||
</NextThemeProvider>
|
||||
)
|
||||
},
|
||||
],
|
||||
|
@ -5,7 +5,6 @@ import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
|
||||
|
||||
/*
|
||||
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
|
||||
* to learn more about the test-runner hooks API.
|
||||
*/
|
||||
const config: TestRunnerConfig = {
|
||||
async preVisit(page) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/storybook",
|
||||
"version": "1.0.0-staging.1",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@ -16,9 +16,11 @@
|
||||
"@repo/config-tailwind": "workspace:*",
|
||||
"@repo/i18n": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@repo/wikipedia-game-solver": "workspace:*",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"next-themes": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import sharedConfig from "@repo/config-tailwind"
|
||||
|
||||
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||
const config = {
|
||||
content: [".storybook/preview.tsx", "../../packages/**/*.tsx"],
|
||||
content: [".storybook/preview.tsx", "../../packages/*/src/**/*.tsx"],
|
||||
presets: [sharedConfig],
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
TZ=UTC
|
||||
HOSTNAME=0.0.0.0
|
||||
PORT=5000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
NEXT_PUBLIC_API_URL=http://127.0.0.1:5500
|
||||
|
@ -2,11 +2,13 @@ FROM node:22.4.1-slim AS node-pnpm
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
ENV DO_NOT_TRACK=1
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM node-pnpm AS builder
|
||||
RUN pnpm install --global turbo@2.0.9
|
||||
COPY ./ ./
|
||||
RUN pnpm install --global turbo@2.1.0
|
||||
RUN turbo prune @repo/website --docker
|
||||
|
||||
FROM node-pnpm AS installer
|
||||
|
@ -1,34 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@repo/ui/design/Button"
|
||||
import { MainLayout } from "@repo/ui/MainLayout"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useEffect } from "react"
|
||||
import type { ErrorServerProps } from "@repo/ui/Errors/ErrorServer"
|
||||
import { ErrorServer } from "@repo/ui/Errors/ErrorServer"
|
||||
|
||||
interface ErrorBoundaryPageProps {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
const ErrorBoundaryPage: React.FC<ErrorBoundaryPageProps> = (props) => {
|
||||
const { error, reset } = props
|
||||
|
||||
const t = useTranslations()
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<MainLayout className="items-center justify-center text-center">
|
||||
<h1 className="text-3xl font-semibold">
|
||||
{t("errors.error")} 500 - {t("errors.server-error")}
|
||||
</h1>
|
||||
<p className="mb-4 mt-2">
|
||||
<Button onClick={reset}>{t("errors.try-again")}</Button>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
const ErrorBoundaryPage: React.FC<ErrorServerProps> = (props) => {
|
||||
return <ErrorServer {...props} />
|
||||
}
|
||||
|
||||
export default ErrorBoundaryPage
|
||||
|
@ -1,10 +1,10 @@
|
||||
import "@repo/config-tailwind/styles.css"
|
||||
import { VERSION } from "@repo/constants"
|
||||
import type { Locale, LocaleProps } from "@repo/i18n/config"
|
||||
import { LOCALES } from "@repo/i18n/config"
|
||||
import { Footer } from "@repo/ui/Footer"
|
||||
import { Header } from "@repo/ui/Header"
|
||||
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
|
||||
import type { LocaleProps } from "@repo/i18n/config"
|
||||
import { Footer } from "@repo/ui/Layout/Footer"
|
||||
import { Header } from "@repo/ui/Layout/Header"
|
||||
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
|
||||
import type { Locale } from "@repo/utils/constants"
|
||||
import { LOCALES, VERSION } from "@repo/utils/constants"
|
||||
import type { Metadata } from "next"
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
import {
|
||||
@ -17,9 +17,37 @@ export const generateMetadata = async ({
|
||||
params,
|
||||
}: LocaleProps): Promise<Metadata> => {
|
||||
const t = await getTranslations({ locale: params.locale })
|
||||
const title = t("meta.title")
|
||||
const description = t("meta.description")
|
||||
const image = "/images/Wikipedia-Logo.webp"
|
||||
const url = new URL("https://wikipedia-game-solver.theoludwig.fr")
|
||||
const locale = LOCALES.join(", ")
|
||||
|
||||
return {
|
||||
title: t("meta.title"),
|
||||
description: t("meta.description"),
|
||||
title,
|
||||
description,
|
||||
metadataBase: url,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
siteName: title,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
width: 96,
|
||||
height: 96,
|
||||
},
|
||||
],
|
||||
locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title,
|
||||
description,
|
||||
images: [image],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,11 +59,7 @@ export const generateStaticParams = (): Array<{ locale: Locale }> => {
|
||||
})
|
||||
}
|
||||
|
||||
interface LocaleLayoutProps extends React.PropsWithChildren {
|
||||
params: {
|
||||
locale: Locale
|
||||
}
|
||||
}
|
||||
interface LocaleLayoutProps extends React.PropsWithChildren, LocaleProps {}
|
||||
|
||||
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
|
||||
const { children, params } = props
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { MainLayout } from "@repo/ui/MainLayout"
|
||||
import { Spinner } from "@repo/ui/design/Spinner"
|
||||
import { Spinner } from "@repo/ui/Design/Spinner"
|
||||
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
return (
|
||||
<MainLayout className="items-center justify-center">
|
||||
<MainLayout center>
|
||||
<Spinner size={50} />
|
||||
</MainLayout>
|
||||
)
|
||||
|
@ -1,24 +1,10 @@
|
||||
import { Link } from "@repo/ui/design/Link"
|
||||
import { MainLayout } from "@repo/ui/MainLayout"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { ErrorNotFound } from "@repo/ui/Errors/ErrorNotFound"
|
||||
|
||||
/**
|
||||
* Note that `app/[locale]/[...rest]/page.tsx` is necessary for this page to render.
|
||||
*/
|
||||
const NotFound: React.FC = () => {
|
||||
const t = useTranslations()
|
||||
|
||||
return (
|
||||
<MainLayout className="items-center justify-center text-center">
|
||||
<h1 className="text-3xl font-semibold">
|
||||
{t("errors.error")} 404 - {t("errors.not-found")}
|
||||
</h1>
|
||||
<p className="mb-4 mt-2">
|
||||
{t("errors.page-doesnt-exist")}{" "}
|
||||
<Link href="/">{t("errors.return-to-home-page")}</Link>
|
||||
</p>
|
||||
</MainLayout>
|
||||
)
|
||||
return <ErrorNotFound />
|
||||
}
|
||||
|
||||
export default NotFound
|
||||
|
@ -1,13 +1,15 @@
|
||||
import type { LocaleProps } from "@repo/i18n/config"
|
||||
import { Link } from "@repo/ui/design/Link"
|
||||
import { Typography } from "@repo/ui/design/Typography"
|
||||
import { MainLayout } from "@repo/ui/MainLayout"
|
||||
import { Link } from "@repo/ui/Design/Link"
|
||||
import { Typography } from "@repo/ui/Design/Typography"
|
||||
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
||||
import { fromLocaleToWikipediaLocale, getWikipediaLink } from "@repo/wikipedia"
|
||||
import { WikipediaClient } from "@repo/wikipedia-game-solver/WikipediaClient"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { unstable_setRequestLocale } from "next-intl/server"
|
||||
import Image from "next/image"
|
||||
|
||||
import WikipediaLogo from "#public/images/Wikipedia-Logo.png"
|
||||
import WikipediaLogo from "#public/images/Wikipedia-Logo.webp"
|
||||
import { Section } from "@repo/ui/Layout/Section"
|
||||
|
||||
interface HomePageProps extends LocaleProps {}
|
||||
|
||||
@ -19,25 +21,27 @@ const HomePage: React.FC<HomePageProps> = (props) => {
|
||||
|
||||
const t = useTranslations()
|
||||
|
||||
const localeWikipedia = fromLocaleToWikipediaLocale(params.locale)
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<section className="text-center">
|
||||
<MainLayout center>
|
||||
<Section horizontalSpacing>
|
||||
<Typography as="h1" variant="h1">
|
||||
{t("home.title")}
|
||||
</Typography>
|
||||
|
||||
<Typography as="p" variant="text1" className="mt-3">
|
||||
{t.rich("home.description", {
|
||||
wikipedia: (children) => {
|
||||
"wikipedia-link": (children) => {
|
||||
return (
|
||||
<Link href="https://en.wikipedia.org/" target="_blank">
|
||||
<Link href={getWikipediaLink(localeWikipedia)} target="_blank">
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
})}
|
||||
</Typography>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<section className="my-6 flex items-center justify-center">
|
||||
<Image src={WikipediaLogo} alt="Wikipedia" className="w-72" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config"
|
||||
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/utils/constants"
|
||||
import createMiddleware from "next-intl/middleware"
|
||||
|
||||
export default createMiddleware({
|
||||
@ -14,6 +14,8 @@ export const config = {
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
// Next.js issue, middleware matcher should support template literals:
|
||||
// https://github.com/vercel/next.js/issues/56398
|
||||
"/(en-US|fr-FR)/:path*",
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
|
5
apps/website/next-env.d.ts
vendored
5
apps/website/next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/website",
|
||||
"version": "1.0.0-staging.1",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
@ -15,15 +15,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/config-tailwind": "workspace:*",
|
||||
"@repo/constants": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@repo/i18n": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/wikipedia-game-solver": "workspace:*",
|
||||
"@repo/wikipedia": "workspace:*",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-icons": "catalog:"
|
||||
"sharp": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 272 KiB |
BIN
apps/website/public/images/Wikipedia-Logo.webp
Normal file
BIN
apps/website/public/images/Wikipedia-Logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
@ -2,11 +2,7 @@ import sharedConfig from "@repo/config-tailwind"
|
||||
|
||||
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||
const config = {
|
||||
content: [
|
||||
"./**/*.tsx",
|
||||
"../../packages/ui/**/*.tsx",
|
||||
"../../packages/wikipedia-game-solver/**/*.tsx",
|
||||
],
|
||||
content: ["./app/**/*.tsx", "../../packages/*/src/**/*.tsx"],
|
||||
presets: [sharedConfig],
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/config-typescript/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"types": ["@total-typescript/ts-reset", "@repo/i18n/messages.d.ts"],
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"jsx": "preserve",
|
||||
"paths": {
|
||||
"#*": ["./*"]
|
||||
},
|
||||
|
34
compose.dev.yaml
Normal file
34
compose.dev.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
services:
|
||||
wikipedia-solver-dev-database:
|
||||
container_name: "wikipedia-solver-dev-database"
|
||||
image: "postgres:16.3"
|
||||
restart: "unless-stopped"
|
||||
env_file: ".env"
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USER}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
command: |
|
||||
--max_wal_size=4GB
|
||||
ports:
|
||||
- "${DATABASE_PORT-5432}:${DATABASE_PORT-5432}"
|
||||
volumes:
|
||||
- "wikipedia-solver-dev-postgres-data:/var/lib/postgresql/data"
|
||||
- "./data:/data/"
|
||||
|
||||
wikipedia-solver-dev-adminer:
|
||||
container_name: "wikipedia-solver-dev-adminer"
|
||||
image: "adminer:4.8.1"
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file: ".env"
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: "wikipedia-solver-dev-database"
|
||||
volumes:
|
||||
- "./data/adminer/default-orange.css:/var/www/html/adminer.css"
|
||||
- "./data/adminer/logo.png:/var/www/html/logo.png"
|
||||
- "./data/adminer/fonts/:/var/www/html/fonts"
|
||||
|
||||
volumes:
|
||||
wikipedia-solver-dev-postgres-data:
|
37
compose.yaml
37
compose.yaml
@ -1,7 +1,7 @@
|
||||
services:
|
||||
wikipedia-game-solver:
|
||||
container_name: "wikipedia-game-solver"
|
||||
image: "wikipedia-game-solver"
|
||||
wikipedia-game-solver-website:
|
||||
container_name: "wikipedia-game-solver-website"
|
||||
image: "wikipedia-game-solver-website"
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: "./"
|
||||
@ -11,3 +11,34 @@ services:
|
||||
environment:
|
||||
PORT: ${WEBSITE_PORT-5000}
|
||||
env_file: "./apps/website/.env"
|
||||
|
||||
wikipedia-game-solver-api:
|
||||
container_name: "wikipedia-game-solver-api"
|
||||
image: "wikipedia-game-solver-api"
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: "./"
|
||||
dockerfile: "./apps/api/Dockerfile"
|
||||
ports:
|
||||
- "${API_PORT-5000}:${API_PORT-5000}"
|
||||
environment:
|
||||
PORT: ${API_PORT-5000}
|
||||
env_file: "./apps/api/.env"
|
||||
|
||||
wikipedia-solver-database:
|
||||
container_name: "wikipedia-solver-database"
|
||||
image: "postgres:16.3"
|
||||
restart: "unless-stopped"
|
||||
env_file: ".env"
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USER}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
command: |
|
||||
--max_wal_size=4GB
|
||||
volumes:
|
||||
- "wikipedia-solver-postgres-data:/var/lib/postgresql/data"
|
||||
- "./data:/data/"
|
||||
|
||||
volumes:
|
||||
wikipedia-solver-postgres-data:
|
||||
|
16
data/.eslintrc.json
Normal file
16
data/.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@repo/eslint-config"],
|
||||
"rules": {
|
||||
"import-x/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never",
|
||||
"tsx": "never",
|
||||
"js": "always",
|
||||
"jsx": "never"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
163
data/README.md
Normal file
163
data/README.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Wikipedia data
|
||||
|
||||
```sh
|
||||
./download-wikipedia-dump.sh
|
||||
node --max-old-space-size=8096 generate-sql-files.js
|
||||
|
||||
# Inside the Database container
|
||||
docker exec -it wikipedia-solver-dev-database sh
|
||||
/data/execute-sql.sh
|
||||
```
|
||||
|
||||
## Utils
|
||||
|
||||
Show the first 10 line of sql file: `head -n 10 ./dump/page.sql`
|
||||
|
||||
Show the first 10 characters of sql file: `head -c 10 ./dump/page.sql`
|
||||
|
||||
To inspect volume size used by database: `docker system df -v`
|
||||
|
||||
## Remove a volume
|
||||
|
||||
```sh
|
||||
# List all volumes
|
||||
docker volume ls
|
||||
|
||||
# Remove a volume
|
||||
docker volume rm data_wikipedia-solver-mariadb-data
|
||||
|
||||
# Or by using docker compose down
|
||||
docker-compose down --volumes
|
||||
```
|
||||
|
||||
## PostgreSQL Related
|
||||
|
||||
<https://stackoverflow.com/questions/12206600/how-to-speed-up-insertion-performance-in-postgresql>
|
||||
|
||||
```sh
|
||||
docker exec -it wikipedia-solver-dev-database sh
|
||||
|
||||
psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}"
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Execute script with inserts
|
||||
\i /data/sql-pages-inserts/0001-pages-inserts.sql
|
||||
|
||||
/data/sql-internal-links-inserts/0001-internal-links.sh
|
||||
```
|
||||
|
||||
## Dumps Links
|
||||
|
||||
- Database layout: <https://www.mediawiki.org/wiki/Manual:Database_layout>
|
||||
- <https://en.wikipedia.org/wiki/Wikipedia:Database_download>
|
||||
- <https://dumps.wikimedia.org/enwiki/>
|
||||
- Run SQL queries against Wikipedia: <https://quarry.wmcloud.org/>
|
||||
|
||||
```sql
|
||||
-- Get the sanitized title of a page linked in the page with title 'Node.js'
|
||||
SELECT lt.lt_title FROM linktarget lt WHERE lt.lt_id = (
|
||||
SELECT pl.pl_target_id FROM pagelinks pl WHERE pl.pl_from = (
|
||||
SELECT p.page_id FROM page p WHERE p.page_title = 'Node.js' AND p.page_namespace = 0
|
||||
) LIMIT 1
|
||||
);
|
||||
```
|
||||
|
||||
## `page.sql.gz` - MySQL full version up until inserts
|
||||
|
||||
```sql
|
||||
-- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: db1206 Database: enwiki
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.4.26-MariaDB-log
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `page`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `page`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `page` (
|
||||
`page_id` int(8) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`page_namespace` int(11) NOT NULL DEFAULT 0,
|
||||
`page_title` varbinary(255) NOT NULL DEFAULT '',
|
||||
`page_is_redirect` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`page_is_new` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`page_random` double unsigned NOT NULL DEFAULT 0,
|
||||
`page_touched` binary(14) NOT NULL,
|
||||
`page_links_updated` varbinary(14) DEFAULT NULL,
|
||||
`page_latest` int(8) unsigned NOT NULL DEFAULT 0,
|
||||
`page_len` int(8) unsigned NOT NULL DEFAULT 0,
|
||||
`page_content_model` varbinary(32) DEFAULT NULL,
|
||||
`page_lang` varbinary(35) DEFAULT NULL,
|
||||
PRIMARY KEY (`page_id`),
|
||||
UNIQUE KEY `page_name_title` (`page_namespace`,`page_title`),
|
||||
KEY `page_random` (`page_random`),
|
||||
KEY `page_len` (`page_len`),
|
||||
KEY `page_redirect_namespace_len` (`page_is_redirect`,`page_namespace`,`page_len`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=76684425 DEFAULT CHARSET=binary ROW_FORMAT=COMPRESSED;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `page`
|
||||
--
|
||||
```
|
||||
|
||||
## `pagelinks.sql.gz` - MySQL full version up until inserts
|
||||
|
||||
```sql
|
||||
-- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: db1206 Database: enwiki
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.4.26-MariaDB-log
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `pagelinks`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `pagelinks`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `pagelinks` (
|
||||
`pl_from` int(8) unsigned NOT NULL DEFAULT 0,
|
||||
`pl_namespace` int(11) NOT NULL DEFAULT 0,
|
||||
`pl_title` varbinary(255) NOT NULL DEFAULT '',
|
||||
`pl_from_namespace` int(11) NOT NULL DEFAULT 0,
|
||||
`pl_target_id` bigint(20) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`pl_from`,`pl_namespace`,`pl_title`),
|
||||
KEY `pl_namespace` (`pl_namespace`,`pl_title`,`pl_from`),
|
||||
KEY `pl_backlinks_namespace` (`pl_from_namespace`,`pl_namespace`,`pl_title`,`pl_from`),
|
||||
KEY `pl_target_id` (`pl_target_id`,`pl_from`),
|
||||
KEY `pl_backlinks_namespace_target_id` (`pl_from_namespace`,`pl_target_id`,`pl_from`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=binary ROW_FORMAT=COMPRESSED;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `pagelinks`
|
||||
--
|
||||
```
|
1376
data/adminer/default-orange.css
Normal file
1376
data/adminer/default-orange.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/adminer/fonts/entypo.eot
Normal file
BIN
data/adminer/fonts/entypo.eot
Normal file
Binary file not shown.
264
data/adminer/fonts/entypo.svg
Normal file
264
data/adminer/fonts/entypo.svg
Normal file
@ -0,0 +1,264 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata></metadata>
|
||||
<defs>
|
||||
<font id="entyporegular" horiz-adv-x="1228" >
|
||||
<font-face units-per-em="2048" ascent="1536" descent="-512" />
|
||||
<missing-glyph horiz-adv-x="500" />
|
||||
<glyph unicode="
" horiz-adv-x="681" />
|
||||
<glyph unicode=" " horiz-adv-x="513" />
|
||||
<glyph unicode=" " horiz-adv-x="1026" />
|
||||
<glyph unicode=" " horiz-adv-x="513" />
|
||||
<glyph unicode=" " horiz-adv-x="1026" />
|
||||
<glyph unicode=" " horiz-adv-x="342" />
|
||||
<glyph unicode=" " horiz-adv-x="256" />
|
||||
<glyph unicode=" " horiz-adv-x="171" />
|
||||
<glyph unicode=" " horiz-adv-x="171" />
|
||||
<glyph unicode=" " horiz-adv-x="128" />
|
||||
<glyph unicode=" " horiz-adv-x="205" />
|
||||
<glyph unicode=" " horiz-adv-x="57" />
|
||||
<glyph unicode="‖" horiz-adv-x="747" d="M553 870q92 0 92 -65v-584q0 -68 -92 -67.5t-92 67.5v584q0 65 92 65zM194.5 870q92.5 0 92.5 -65v-584q0 -68 -92.5 -67.5t-92.5 67.5v584q0 65 92.5 65z" />
|
||||
<glyph unicode=" " horiz-adv-x="205" />
|
||||
<glyph unicode=" " horiz-adv-x="256" />
|
||||
<glyph unicode="ℹ" horiz-adv-x="675" d="M463 1024q49 0 75.5 -27.5t26.5 -70.5q0 -51 -40 -90t-97 -39q-49 0 -75.5 26.5t-24.5 73.5q0 47 36 87t99 40zM252 0q-102 0 -55 182l61 260q14 57 0 58q-12 0 -55 -18.5t-74 -39.5l-27 45q92 80 193.5 129.5t155.5 49.5q80 0 36 -166l-71 -273q-16 -66 6 -65 q45 0 121 61l30 -41q-86 -88 -179 -135t-142 -47z" />
|
||||
<glyph unicode="←" horiz-adv-x="1208" d="M348 256l-246 256l246 256v-164h758v-182h-758v-166z" />
|
||||
<glyph unicode="↑" horiz-adv-x="716" d="M614 770h-165v-760h-181v760h-166l256 244z" />
|
||||
<glyph unicode="→" horiz-adv-x="1208" d="M862 256v166h-760v182h760v164l244 -256z" />
|
||||
<glyph unicode="↓" horiz-adv-x="716" d="M614 256l-256 -246l-256 246h166v758h181v-758h165z" />
|
||||
<glyph unicode="↰" horiz-adv-x="1075" d="M307 512v-92l-205 164l205 174v-103h563q41 0 72 -29.5t31 -72.5v-287h-144v246h-522z" />
|
||||
<glyph unicode="↳" horiz-adv-x="966" d="M205 358q-43 0 -73 31t-30 72v358h144v-317h372v153l246 -225l-246 -225v153h-413z" />
|
||||
<glyph unicode="⇆" d="M819 760v-144h-512v-92l-205 164l205 174v-102h512zM1126 330l-204 -164v92h-512v143h512v103z" />
|
||||
<glyph unicode="∞" d="M918 737q86 0 147 -54t61 -171q0 -115 -61 -170t-147 -55q-80 0 -161 41t-143 108q-59 -68 -140 -108.5t-161 -40.5q-88 0 -149.5 55t-61.5 170q0 117 61.5 171t149.5 54q80 0 161 -40t140 -107q59 68 141.5 107.5t162.5 39.5zM313 377q61 0 130 38t116 97 q-47 59 -114.5 97t-131.5 38q-117 0 -117 -135t117 -135zM918 377q117 0 116.5 135t-116.5 135q-63 0 -132 -38t-114 -97q45 -59 114 -97t132 -38z" />
|
||||
<glyph unicode="⊕" horiz-adv-x="1064" d="M532.5 942q178.5 0 304.5 -126t126 -304t-126 -304t-304.5 -126t-304.5 126t-126 304t126 304t304.5 126zM586 461h205v104h-205v207h-105v-207h-207v-104h207v-207h105v207z" />
|
||||
<glyph unicode="⊖" horiz-adv-x="1064" d="M532.5 942q178.5 0 304.5 -126t126 -304t-126 -304t-304.5 -126t-304.5 126t-126 304t126 304t304.5 126zM791 565h-517v-104h517v104z" />
|
||||
<glyph unicode="⊞" horiz-adv-x="1024" d="M819 922q43 0 73 -30t30 -73v-614q0 -41 -30 -72t-73 -31h-614q-41 0 -72 31t-31 72v614q0 43 31 73t72 30h614zM768 461v102h-205v205h-102v-205h-205v-102h205v-205h102v205h205z" />
|
||||
<glyph unicode="⊟" horiz-adv-x="1024" d="M819 922q43 0 73 -30t30 -73v-614q0 -41 -30 -72t-73 -31h-614q-41 0 -72 31t-31 72v614q0 43 31 73t72 30h614zM768 461v102h-512v-102h512z" />
|
||||
<glyph unicode="⌂" horiz-adv-x="1128" d="M1012 498q16 -16 11 -27.5t-28 -11.5h-86v-318q0 -14 -1 -21t-8 -13.5t-23 -6.5h-209v318h-209v-318h-199q-29 0 -36 10.5t-7 30.5v318h-86q-23 0 -28 11t12 28l409 411q16 16 39 16.5t39 -16.5z" />
|
||||
<glyph unicode="⌨" d="M1055 819q29 0 50 -21.5t21 -49.5v-472q0 -31 -21.5 -51t-49.5 -20h-881q-29 0 -50.5 20.5t-21.5 50.5v472q0 29 21.5 50t50.5 21h881zM666 717v-103h102v103h-102zM819 563h-102v-102h102v102zM512 717v-103h102v103h-102zM666 563h-103v-102h103v102zM358 717v-103h103 v103h-103zM512 563h-102v-102h102v102zM205 717v-103h102v103h-102zM358 563h-102v-102h102v102zM307 307v103h-102v-103h102zM870 307v103h-512v-103h512zM1024 307v103h-102v-103h102zM870 461h103v102h-103v-102zM1024 614v103h-205v-103h205z" />
|
||||
<glyph unicode="⌫" d="M1024 870q43 0 72.5 -29.5t29.5 -72.5v-512q0 -41 -29.5 -71.5t-72.5 -30.5h-489q-39 0 -72 28l-348 303q-29 27 0 56l348 303q31 27 72 26h489zM881 307l73 76l-131 129l131 131l-73 74l-131 -129l-132 129l-73 -74l131 -131l-131 -129l73 -76l132 131z" />
|
||||
<glyph unicode="⏩" horiz-adv-x="1105" d="M989 537q14 -10 15 -25q0 -14 -15 -23l-381 -253q-23 -14 -38 -6t-15 36v494q0 29 15.5 37t37.5 -6zM524 537q14 -10 15 -25q0 -14 -15 -23l-368 -253q-20 -14 -37 -6t-17 36v494q0 29 16.5 37t37.5 -6z" />
|
||||
<glyph unicode="⏪" horiz-adv-x="1105" d="M102 512q0 14 15 25l383 254q20 14 36.5 5.5t16.5 -36.5v-494q0 -29 -16.5 -37t-36.5 7l-383 253q-15 9 -15 23zM567 512q0 14 15 25l368 254q20 14 37 5.5t17 -36.5v-494q0 -29 -16.5 -37t-37.5 7l-368 253q-15 9 -15 23z" />
|
||||
<glyph unicode="⏭" horiz-adv-x="819" d="M524 537q14 -10 15 -25q0 -12 -15 -23l-370 -233q-23 -14 -37.5 -5t-14.5 36v452q0 27 14.5 36t37.5 -5zM641 811q76 0 76 -59v-478q0 -59 -76 -59q-78 0 -78 59v478q0 59 78 59z" />
|
||||
<glyph unicode="⏮" horiz-adv-x="819" d="M281 512q0 14 14 25l373 233q20 14 34.5 5t14.5 -36v-452q0 -27 -14.5 -36t-34.5 5l-373 233q-14 11 -14 23zM102 752q0 59 78 59q76 0 76 -59v-478q0 -59 -76 -59q-78 0 -78 59v478z" />
|
||||
<glyph unicode="⏳" horiz-adv-x="778" d="M676 791q0 -45 -49 -98.5t-99.5 -101.5t-50.5 -79t50.5 -78t99.5 -99t49 -99v-121q0 -35 -88 -75t-198.5 -40t-199 40t-88.5 75v121q0 47 49.5 99t99.5 99t50 78t-50 79t-99.5 101t-49.5 99v120q0 33 89.5 73t198 40t197.5 -40t89 -73v-120zM182 905l-18 -14q-4 -8 4 -14 q94 -53 221 -54q135 0 225 51q14 10 -16 31q-98 55 -207 56q-123 -1 -209 -56zM416 512q0 18 4 33.5t18.5 34t20.5 25.5t31.5 32t29.5 29q94 94 94 125l2 51q-102 -55 -227 -55.5t-227 55.5l4 -51q0 -33 92 -125q6 -6 22.5 -21.5t23.5 -23t19.5 -19.5t17.5 -21.5t11 -20.5 t9.5 -23.5t3.5 -24.5q0 -10 -1.5 -19.5t-6.5 -18.5t-8 -16t-11 -17.5t-12 -15.5t-15.5 -16.5t-16.5 -15.5t-18.5 -16t-17.5 -17q-92 -92 -92 -124v-68q8 4 67.5 23.5t94 44t34.5 59.5q0 31 27 31t27 -31q0 -35 33.5 -59.5t96 -44t68.5 -23.5v68q0 31 -94 124 q-4 4 -21.5 20.5t-22.5 22t-18.5 19.5t-18.5 22.5t-12 20.5t-9 23.5t-2 23.5z" />
|
||||
<glyph unicode="⏴" horiz-adv-x="430" d="M215 625q47 0 80 -33t33 -80q0 -45 -33 -79t-80 -34t-80 34t-33 79q0 47 33 80t80 33z" />
|
||||
<glyph unicode="⏵" horiz-adv-x="788" d="M215 625q47 0 80 -33t33 -80q0 -45 -33 -79t-80 -34t-80 33t-33 80t33 80t80 33zM573.5 625q47.5 0 80 -33t32.5 -80q0 -45 -34 -79t-79 -34q-47 0 -79.5 33t-32.5 80t32.5 80t80 33z" />
|
||||
<glyph unicode="⏶" horiz-adv-x="1146" d="M215 625q47 0 80 -33t33 -80q0 -45 -33 -79t-80 -34t-80 34t-33 79q0 47 33 80t80 33zM573.5 625q47.5 0 80 -33t32.5 -80q0 -45 -34 -79t-79 -34t-78.5 34t-33.5 79q0 47 32.5 80t80 33zM932 625q47 0 79.5 -33t32.5 -80q0 -45 -32.5 -79t-79.5 -34t-80 34t-33 79 q0 47 33 80t80 33z" />
|
||||
<glyph unicode="⏷" horiz-adv-x="1122" d="M1020 338q0 -35 -24.5 -58.5t-57.5 -23.5h-799q-23 0 -32 5t-2.5 15.5t24.5 20.5l821 463q29 18 49.5 7t20.5 -46v-383z" />
|
||||
<glyph unicode="■" horiz-adv-x="819" d="M641 819q76 0 76 -65v-482q0 -68 -76 -67h-461q-78 0 -78 67v482q0 37 18.5 51t59.5 14h461z" />
|
||||
<glyph unicode="▴" horiz-adv-x="675" d="M102 307l236 410l235 -410h-471z" />
|
||||
<glyph unicode="▶" horiz-adv-x="716" d="M600 539q14 -10 14 -27q0 -14 -14 -25l-438 -272q-25 -16 -42.5 -6t-17.5 41v526q0 31 17.5 41t42.5 -6z" />
|
||||
<glyph unicode="▸" horiz-adv-x="614" d="M102 748l410 -236l-410 -236v472z" />
|
||||
<glyph unicode="▾" horiz-adv-x="675" d="M573 717l-235 -410l-236 410h471z" />
|
||||
<glyph unicode="◂" horiz-adv-x="614" d="M512 748v-472l-410 236z" />
|
||||
<glyph unicode="●" horiz-adv-x="921" d="M460.5 870q149.5 0 254 -104t104.5 -254q0 -147 -104.5 -252.5t-254 -105.5t-254 105.5t-104.5 252.5q0 150 104.5 254t254 104z" />
|
||||
<glyph unicode="◑" d="M1075 553q20 0 35.5 -12.5t15.5 -28.5q0 -41 -51 -41h-49q-51 0 -51 41q0 16 15.5 28.5t35.5 12.5h49zM614.5 793q116.5 0 199.5 -82t83 -199q0 -119 -83 -201t-199.5 -82t-198.5 82t-82 201q0 117 82 199t198.5 82zM621 307v410q-88 0 -149.5 -60.5t-61.5 -144.5 q0 -86 61.5 -145.5t149.5 -59.5zM256 512q0 -41 -51 -41h-51q-51 0 -52 41q0 16 15.5 28.5t36.5 12.5h51q20 0 35.5 -12.5t15.5 -28.5zM614.5 870q-16.5 0 -29 15.5t-12.5 36.5v51q0 20 12.5 35.5t29 15.5t28.5 -15.5t12 -35.5v-51q0 -20 -12 -36t-28.5 -16zM614.5 154 q16.5 0 28.5 -15.5t12 -36.5v-51q0 -20 -12 -35.5t-28.5 -15.5t-29 15.5t-12.5 35.5v51q0 20 12.5 36t29 16zM991 829l-35 -34q-35 -35 -65 -9q-29 29 8 66q4 6 35 37q37 35 65.5 6t-8.5 -66zM274 227q14 16 34 18.5t30 -9.5q12 -12 10 -32t-16 -34l-37 -37 q-14 -14 -33.5 -16t-30.5 10q-31 29 7 66q5 3 36 34zM295 889l37 -37q37 -37 6 -66q-10 -10 -29.5 -8t-34.5 17q-31 31 -36 34q-14 14 -16.5 34t9.5 32q10 12 30 10t34 -16zM899 170q-37 37 -8 65.5t65 -8.5l35 -34q37 -37 8.5 -66t-65.5 6q-31 31 -35 37z" />
|
||||
<glyph unicode="◴" horiz-adv-x="1064" d="M479 942v-377h-377q18 150 124 255.5t253 121.5zM588 942q160 -20 267.5 -142t107.5 -286q0 -178 -126 -305t-307 -127q-164 0 -284.5 107.5t-143.5 269.5h435q20 0 35.5 14t15.5 37v432z" />
|
||||
<glyph unicode="◼" horiz-adv-x="1024" d="M0 0v0v0v0v0z" />
|
||||
<glyph unicode="☁" d="M881 659q102 0 173.5 -69.5t71.5 -170t-71.5 -170t-173.5 -69.5h-592q-76 0 -131.5 53.5t-55.5 126.5q0 76 54.5 129.5t132.5 53.5q2 0 10 -1t10 -1q-2 12 -2 39q0 111 80 188.5t193 77.5q92 0 163.5 -53.5t96.5 -137.5q29 4 41 4z" />
|
||||
<glyph unicode="★" horiz-adv-x="1105" d="M553 963l123 -345h328l-269 -200l96 -357l-278 213l-279 -213l97 357l-269 200h328z" />
|
||||
<glyph unicode="☆" horiz-adv-x="1105" d="M1004 618l-269 -200l96 -357l-278 213l-279 -213l97 357l-269 200h328l123 345l123 -345h328zM553 375l154 -127l-64 182l148 117l-181 -4l-57 207l-55 -207l-181 4l146 -117l-64 -182z" />
|
||||
<glyph unicode="☕" horiz-adv-x="905" d="M453 932q156 0 255 -42t93 -89l-74 -608q-2 -14 -35 -37t-99.5 -43.5t-140 -20.5t-139 20.5t-99.5 43t-36 37.5l-74 608q-4 29 37 58.5t124.5 51t187.5 21.5zM452.5 711q73.5 0 140 15t100.5 33.5t34 31t-34 30t-100.5 32.5t-140 15t-140 -15t-100.5 -32.5t-34 -30 t34 -31t100.5 -33.5t140 -15z" />
|
||||
<glyph unicode="☰" horiz-adv-x="921" d="M768 563q23 0 37 -15.5t14 -35.5t-15.5 -35.5t-35.5 -15.5h-614q-20 0 -36 15.5t-16 35.5t14.5 35.5t37.5 15.5h614zM154 666q-20 0 -36 15t-16 35.5t14.5 36t37.5 15.5h614q23 0 37 -15.5t14 -36t-15.5 -35.5t-35.5 -15h-614zM768 358q23 0 37 -15t14 -35.5t-15.5 -36 t-35.5 -15.5h-614q-20 0 -36 15.5t-16 36t14.5 35.5t37.5 15h614z" />
|
||||
<glyph unicode="☽" horiz-adv-x="1046" d="M639 397.5q109 108.5 128 258t-54 276.5q53 -27 98 -74q131 -131 131 -316.5t-131 -316.5t-317.5 -131t-317.5 131q-41 41 -74 97q127 -72 277.5 -52.5t259.5 128z" />
|
||||
<glyph unicode="♡" horiz-adv-x="1089" d="M913 811q72 -66 72 -160t-72 -159l-368 -338l-369 338q-72 66 -72 159.5t72 159.5q66 59 156 59t153 -59l60 -53l57 53q66 59 155.5 59t155.5 -59zM858 545q43 41 43 106q0 68 -39 103q-39 39 -104 39q-53 0 -107 -50l-106 -94l-109 94q-49 49 -104 50q-66 0 -107 -39 q-39 -37 -39 -103q0 -68 45 -106l314 -293z" />
|
||||
<glyph unicode="♥" horiz-adv-x="1089" d="M913 813q72 -66 72 -160t-72 -161l-368 -338l-369 338q-72 68 -72 162t72 159q63 59 154.5 59t156.5 -59l58 -53l59 53q63 59 153 59t156 -59z" />
|
||||
<glyph unicode="♪" horiz-adv-x="800" d="M494 993q0 -43 47 -99t92 -102.5t62.5 -122t-40.5 -157.5q-20 -35 -26 -16q-2 6 0 16q10 18 6 57t-13.5 80t-44 75t-83.5 42v-549q2 -49 -38 -97t-108 -73q-76 -29 -146.5 -8.5t-91 78t20.5 118t119 87.5q88 31 162 4v667h82z" />
|
||||
<glyph unicode="♫" horiz-adv-x="964" d="M313 885l547 119v-721q2 -43 -30.5 -83t-88.5 -61q-66 -25 -112.5 -8.5t-63.5 66.5q-18 49 7.5 99t86.5 75q53 20 109 10v385l-362 -84v-502q0 -43 -33 -83t-88 -60q-66 -23 -112 -7.5t-62 64.5q-18 49 6 99.5t86 74.5q55 20 110 11v606z" />
|
||||
<glyph unicode="⚏" horiz-adv-x="819" d="M276 819q82 0 82 -82v-92q0 -82 -82 -82h-92q-82 0 -82 82v92q0 82 82 82h92zM635 819q82 0 82 -82v-92q0 -82 -82 -82h-92q-82 0 -82 82v92q0 82 82 82h92zM276 461q82 0 82 -82v-92q0 -82 -82 -82h-92q-82 0 -82 82v92q0 82 82 82h92zM635 461q82 0 82 -82v-92 q0 -82 -82 -82h-92q-82 0 -82 82v92q0 82 82 82h92z" />
|
||||
<glyph unicode="⚑" horiz-adv-x="1128" d="M997 784q14 6 22.5 -1t0.5 -19q-98 -141 -168 -218t-113 -92.5t-74.5 -2t-61.5 38t-64.5 41t-95 -4t-142.5 -88.5l92 -360h-102l-189 737l95 35q92 68 155.5 88t100 3t65.5 -52t63.5 -73t81 -63.5t132 -20.5t202.5 52z" />
|
||||
<glyph unicode="⚒" horiz-adv-x="1226" d="M260 672q-8 -8 -11 -22.5t-3 -26t-2 -11.5q-2 -2 -17.5 -15t-19.5 -17q-16 -14 -29 4l-72 78q-10 12 3 24q2 2 18 14.5t20 16.5q6 6 28 6t38 14q14 14 18.5 39t10.5 31q2 0 9 7t26.5 22.5t41.5 31.5q137 92 191 99q125 0 152 -2q12 0 -9 -9q-123 -53 -155 -77 q-82 -57 -37 -117q35 -47 39 -49q8 -8 -2 -15q-2 -2 -39 -35.5t-39 -35.5q-14 -8 -19 -4q-43 49 -72.5 61.5t-68.5 -12.5zM553 645l420 -487q18 -23 -2 -39l-49 -43q-23 -14 -39 4l-424 483q-8 8 0 21l73 63q13 8 21 -2zM1120 852q16 -106 -16 -170q-51 -90 -158 -64 q-57 12 -102 -32l-84 -80l-70 80l70 71q25 25 32 54.5t6 66.5t5 60q12 57 143 114q12 6 18.5 -3t2.5 -15q-12 -12 -47 -82q-14 -10 -12.5 -35.5t40.5 -54.5q59 -41 99 22q6 12 26.5 42t22.5 34q4 10 13 9t11 -17zM242 152l260 254l78 -89l-252 -247q-20 -20 -39 -4l-47 47 q-23 19 0 39z" />
|
||||
<glyph unicode="⚙" horiz-adv-x="1064" d="M881 512q0 -74 82 -125q-12 -41 -35 -84q-72 18 -140 -45q-55 -59 -34 -139q-41 -20 -86 -37q-47 84 -135.5 84t-135.5 -84q-45 16 -86 37q20 82 -35 139q-55 55 -139 35q-14 27 -35 84q84 53 84 135q0 74 -84 127q20 57 35 84q76 -18 139 45q55 57 35 139q43 23 86 35 q47 -82 135.5 -82t135.5 82q43 -12 86 -35q-20 -80 34 -139q68 -63 140 -45q23 -43 35 -84q-82 -51 -82 -127zM532.5 326q77.5 0 132 54t54.5 132t-54.5 133t-132 55t-132 -55t-54.5 -133t54.5 -132t132 -54z" />
|
||||
<glyph unicode="⚠" horiz-adv-x="1191" d="M1083 129q10 -16 0 -35q-10 -16 -30 -16h-914q-18 0 -28 16q-12 18 -2 35l456 801q8 18 30.5 18t31.5 -18zM653 180v103h-112v-103h112zM653 358v308h-112v-308h112z" />
|
||||
<glyph unicode="⚡" horiz-adv-x="618" d="M145 51q-4 4 36 96.5t81 186.5t39 100t-96 46t-101 57q-4 12 86.5 122.5t184.5 214t98 99.5q6 -4 -76 -190.5t-80 -190.5t97.5 -44t99.5 -59q4 -20 -178.5 -232t-190.5 -206z" />
|
||||
<glyph unicode="⛈" d="M881 659q102 0 173.5 -69.5t71.5 -170t-71.5 -170t-173.5 -69.5h-592q-76 0 -131.5 53.5t-55.5 126.5q0 76 54.5 129.5t132.5 53.5q2 0 10 -1t10 -1q-2 12 -2 39q0 111 80 188.5t193 77.5q92 0 163.5 -53.5t96.5 -137.5q29 4 41 4zM684 438q14 16 14 31q0 20 -30 33h-4 q-27 14 -39 16l51 119q6 0 6 20q0 14 -8 19q-16 10 -35 -8q-2 -2 -30.5 -33t-62.5 -68t-46 -53q-12 -18 -13 -31q0 -23 31 -30l4 -2q8 -4 39 -17l-53 -117l-2 -8q-2 -8 -2 -14q0 -10 8 -19q18 -10 35 11q102 102 137 151z" />
|
||||
<glyph unicode="✇" d="M891 748q98 0 166.5 -69t68.5 -167q0 -96 -68.5 -166t-166.5 -70h-553q-96 0 -166 70t-70 166q0 98 70 167t166 69q98 0 166.5 -69t68.5 -167q0 -74 -41 -133h164q-41 66 -41 133q0 98 70 167t166 69zM205 512q0 -53 39 -93t94 -40t94 40t39 93q0 55 -39 94t-94 39 t-94 -39t-39 -94zM891 379q55 0 94 40t39 93q0 55 -39 94t-94 39t-94 -39t-39 -94q0 -53 39 -93t94 -40z" />
|
||||
<glyph unicode="✈" d="M377 31l127 409h-185l-114 -102h-103l82 174l-82 174h103l114 -102h185l-127 409h102l230 -409h264h16t37 -4.5t47.5 -11.5t36.5 -21.5t16 -34.5q0 -33 -38.5 -50.5t-75.5 -19.5l-39 -2h-264l-230 -409h-102z" />
|
||||
<glyph unicode="✉" horiz-adv-x="1126" d="M133 754q-33 18 -29 41q2 14 27 14h866q39 0 21 -33q-8 -14 -25 -22q-14 -6 -196.5 -104.5t-186.5 -100.5q-16 -10 -47 -10q-29 0 -47 10q-4 2 -186.5 100.5t-196.5 104.5zM1004 651q20 10 20 -10v-377q0 -16 -17.5 -32.5t-33.5 -16.5h-819q-16 0 -34 16.5t-18 32.5v377 q0 20 21 10l393 -205q18 -10 47 -10t47 10z" />
|
||||
<glyph unicode="✎" horiz-adv-x="1005" d="M838 850q33 -33 48 -65.5t15 -49.5v-16l-258 -258l-297 -295l-244 -53l52 245l297 295l258 258q55 13 129 -61zM332 195l24 24q-2 45 -53 96q-23 23 -46.5 36.5t-35.5 13.5l-14 2l-23 -25l-18 -82q29 -16 47 -35q25 -25 37 -49z" />
|
||||
<glyph unicode="✒" horiz-adv-x="921" d="M166 12q-6 -20 -27 -8q-18 8 -16 35q4 102 51 231q-102 158 -53 324q10 -33 32.5 -80t45 -82t32.5 -31q8 4 0 85t-11 170t26 161q23 45 82 96.5t106 71.5q-25 -47 -34 -96t-4 -80t22 -33q12 0 86 123t108 125q47 4 117 -29.5t84 -66.5q12 -25 0 -81t-41 -85 q-45 -45 -149.5 -63.5t-116.5 -24.5q-16 -10 12 -35q55 -49 180 -21q-57 -82 -139 -116.5t-135 -38.5t-55 -10q-4 -25 50 -55.5t103 -14.5q-31 -57 -64.5 -86t-55 -36t-78 -11t-86.5 -8q-21 -66 -72 -230z" />
|
||||
<glyph unicode="✓" horiz-adv-x="890" d="M358 154q-35 0 -57 28l-184 242q-16 25 -12 53.5t26.5 47t52 14.5t47.5 -29l121 -158l303 486q16 25 44 30.5t55 -8.5q25 -16 31 -44t-9 -54l-358 -574q-20 -33 -58 -32z" />
|
||||
<glyph unicode="✖" horiz-adv-x="1064" d="M532.5 942q178.5 0 304.5 -126t126 -304t-126 -304t-304.5 -126t-304.5 126t-126 304t126 304t304.5 126zM621 512l157 158l-88 88l-158 -156l-155 156l-90 -88l157 -158l-157 -156l90 -88l155 156l158 -156l88 88z" />
|
||||
<glyph unicode="❌" horiz-adv-x="688" d="M567 352q18 -18 18.5 -43.5t-18.5 -44.5q-18 -16 -43.5 -16t-44.5 16l-135 156l-135 -156q-18 -16 -44 -16t-44 16q-16 18 -16.5 44t16.5 44l141 160l-141 162q-16 18 -16.5 43.5t16.5 44.5q18 16 43.5 16t44.5 -16l135 -156l135 156q18 16 44 16t44 -16q18 -18 18.5 -44 t-18.5 -44l-141 -162z" />
|
||||
<glyph unicode="❎" horiz-adv-x="1024" d="M819 922q43 0 73 -30t30 -73v-614q0 -41 -30 -72t-73 -31h-614q-41 0 -72 31t-31 72v614q0 43 31 73t72 30h614zM670 268l88 88l-158 156l158 158l-88 88l-158 -156l-156 156l-90 -88l158 -158l-158 -156l90 -88l156 156z" />
|
||||
<glyph unicode="❓" horiz-adv-x="798" d="M608 911q88 -63 88 -188q0 -66 -43 -127q-12 -20 -90 -82l-47 -31q-41 -35 -49 -61q-6 -16 -8 -45q0 -14 -17 -15h-131q-16 0 -16 13q4 100 29 127q16 23 49 49.5t57 42.5l25 14q23 16 34 35q29 45 29 72q0 41 -26 80q-29 37 -95 36q-70 0 -96 -45q-29 -43 -29 -94h-170 q6 166 117 238q72 43 170 43q133 -1 219 -62zM387 260q45 0 75 -30.5t27 -75.5q-2 -47 -32.5 -75t-75.5 -26q-45 0 -75 30t-27.5 77t33 74.5t75.5 25.5z" />
|
||||
<glyph unicode="❞" horiz-adv-x="985" d="M252 850q150 0 188 -150q39 -143 -41 -309q-82 -172 -229 -209q-33 -8 -68 -8v72q115 0 187 110q55 88 26 150q-16 37 -63 37q-61 0 -105.5 45t-44.5 108.5t44.5 108.5t105.5 45zM682 850q150 0 188 -150q39 -143 -41 -309q-82 -172 -229 -209q-33 -8 -68 -8v72 q115 0 187 110q55 88 26 150q-16 37 -63 37q-61 0 -105.5 45t-44.5 108.5t44.5 108.5t105.5 45z" />
|
||||
<glyph unicode="➕" horiz-adv-x="798" d="M666 563q31 0 30.5 -51t-30.5 -51h-215v-215q0 -31 -51.5 -31t-51.5 31v215h-215q-31 0 -31 51t31 51h215v215q0 31 51.5 31t51.5 -31v-215h215z" />
|
||||
<glyph unicode="➖" horiz-adv-x="798" d="M666 563q31 0 30.5 -51t-30.5 -51h-533q-31 0 -31 51t31 51h533z" />
|
||||
<glyph unicode="➡" horiz-adv-x="952" d="M461 850l389 -338l-389 -338v197h-359v284h359v195z" />
|
||||
<glyph unicode="➢" horiz-adv-x="1085" d="M971 940q8 -8 11 -16.5t-2 -22.5t-10 -26.5t-19.5 -40t-24.5 -50.5q-55 -115 -150.5 -293t-161.5 -298l-67 -121l-55 389l-390 57q453 252 713 377q20 10 49 25.5t40 20.5t25.5 9t24 1t17.5 -11zM877 842l-312 -287l29 -240z" />
|
||||
<glyph unicode="➦" horiz-adv-x="1126" d="M655 412q-215 0 -340 -46.5t-213 -201.5q4 20 13.5 54t51.5 120t98.5 151.5t159.5 120t230 54.5v196l369 -330l-369 -342v224z" />
|
||||
<glyph unicode="⟲" horiz-adv-x="1167" d="M647 907q174 0 296 -122.5t122 -297t-122 -297t-296 -122.5q-145 0 -258 90l72 75q86 -61 186 -61q129 0 221.5 92t92.5 223t-92.5 223.5t-221.5 92.5q-127 0 -219 -89t-94 -216h145l-188 -209l-189 209h127q2 170 125 289.5t293 119.5z" />
|
||||
<glyph unicode="⟳" horiz-adv-x="1167" d="M520 932q172 0 294 -119t126 -289h125l-188 -211l-189 211h148q-4 127 -96.5 215t-219.5 88q-129 0 -221 -92t-92 -223q0 -129 92 -221t221 -92q106 0 187 61l71 -78q-113 -90 -258 -90q-172 0 -295 123t-123 297t123 297t295 123z" />
|
||||
<glyph unicode="⬅" horiz-adv-x="952" d="M489 174l-387 338l387 338v-195h361v-284h-361v-197z" />
|
||||
<glyph unicode="⬆" horiz-adv-x="880" d="M778 498h-196v-359h-283v359h-197l338 389z" />
|
||||
<glyph unicode="⬇" horiz-adv-x="880" d="M778 528l-338 -389l-338 389h197v359h283v-359h196z" />
|
||||
<glyph unicode="" d="M461 563q23 0 37 -15.5t14 -35.5t-15.5 -35.5t-35.5 -15.5h-307q-20 0 -36 15.5t-16 35.5t14.5 35.5t37.5 15.5h307zM461 358q23 0 37 -15t14 -35.5t-15.5 -36t-35.5 -15.5h-307q-20 0 -36 15.5t-16 36t14.5 35.5t37.5 15h307zM1096 563q31 0 30.5 -51t-30.5 -51h-174 v-174q0 -31 -51.5 -31t-51.5 31v174h-168q-31 0 -30.5 51t30.5 51h168v174q0 31 51.5 31t51.5 -31v-174h174zM461 768q23 0 37 -15.5t14 -36t-15.5 -35.5t-35.5 -15h-307q-20 0 -36 15t-16 35.5t14.5 36t37.5 15.5h307z" />
|
||||
<glyph unicode="" horiz-adv-x="921" d="M205 358q20 0 35.5 -15t15.5 -35.5t-15.5 -36t-35.5 -15.5h-51q-20 0 -36 15.5t-16 36t14.5 35.5t37.5 15h51zM205 563q20 0 35.5 -15.5t15.5 -35.5t-15.5 -35.5t-35.5 -15.5h-51q-20 0 -36 15.5t-16 35.5t14.5 35.5t37.5 15.5h51zM205 768q20 0 35.5 -15.5t15.5 -36 t-15.5 -35.5t-35.5 -15h-51q-20 0 -36 15t-16 35.5t14.5 36t37.5 15.5h51zM410 666q-20 0 -36 15t-16 35.5t15.5 36t36.5 15.5h358q23 0 37 -15.5t14 -36t-15.5 -35.5t-35.5 -15h-358zM768 563q23 0 37 -15.5t14 -35.5t-15.5 -35.5t-35.5 -15.5h-358q-20 0 -36 15.5 t-16 35.5t15.5 35.5t36.5 15.5h358zM768 358q23 0 37 -15t14 -35.5t-15.5 -36t-35.5 -15.5h-358q-20 0 -36 15.5t-16 36t15.5 35.5t36.5 15h358z" />
|
||||
<glyph unicode="" horiz-adv-x="952" d="M489 901v-194h361v-388h-361v-196l-387 389z" />
|
||||
<glyph unicode="" horiz-adv-x="952" d="M461 901l389 -389l-389 -389v196h-359v388h359v194z" />
|
||||
<glyph unicode="" horiz-adv-x="983" d="M881 498h-197v-359h-385v359h-197l390 389z" />
|
||||
<glyph unicode="" horiz-adv-x="983" d="M881 528l-389 -389l-390 389h197v359h385v-359h197z" />
|
||||
<glyph unicode="" d="M737 285q184 -66 185 -125v-109h-820v207q37 14 84 27q96 35 132 70.5t36 97.5q0 23 -23.5 49t-31.5 76q-2 12 -23.5 25t-25.5 63q0 16 5 26t9 13l4 4q-8 51 -12 90q-6 55 41 114.5t164 59.5t164 -59.5t43 -114.5l-15 -90q18 -8 19 -43q-2 -29 -9.5 -44.5t-14.5 -17.5 t-14 -8t-9 -18q-10 -47 -34 -75t-24 -50q0 -61 37 -97t133 -71zM973 563h153v-102h-153v-154h-103v154h-153v102h153v154h103v-154z" />
|
||||
<glyph unicode="" horiz-adv-x="1150" d="M569.5 983q194.5 2 333.5 -133t144 -330q2 -195 -134.5 -335t-330.5 -144q-195 -2 -335.5 134t-142.5 331q-4 195 133.5 335t332 142zM567 225q31 0 50.5 19.5t19.5 48.5q2 31 -17.5 50t-50.5 19h-2q-29 0 -48 -18t-21 -47q0 -31 19.5 -50.5t47.5 -21.5h2zM737 561 q27 35 27 80q0 80 -55 119q-53 39 -138 39q-66 0 -106 -27q-70 -43 -74 -149v-5h113v5q0 27 16 55q16 25 55 24q41 0 54 -20q16 -20 16 -45q0 -18 -16 -41q-8 -12 -21 -21l-6 -4l-16.5 -11t-20.5 -15t-21.5 -17.5t-17.5 -17.5q-14 -20 -18 -80v-8h110v4q0 12 5 29 q6 20 28 37l29 18q47 35 57 51z" />
|
||||
<glyph unicode="" horiz-adv-x="1150" d="M569.5 983q194.5 2 333.5 -133t144 -330q2 -195 -134.5 -335t-330.5 -144q-195 -2 -335.5 134t-142.5 331q-4 195 133.5 335t332 142zM623 827q-43 0 -67 -24.5t-24 -50.5q-2 -29 15.5 -45.5t50.5 -16.5q39 0 62.5 22.5t23.5 55.5q0 59 -61 59zM500 219q31 0 86 26.5 t108 80.5l-18 24q-49 -37 -74 -37q-14 0 -4 39l43 164q27 98 -23 98q-31 0 -91 -29.5t-117 -76.5l16 -27q53 35 76 35q12 0 0 -35l-37 -155q-26 -107 35 -107z" />
|
||||
<glyph unicode="" d="M614.5 799q94.5 0 181.5 -25.5t144 -63.5t101 -79t64.5 -73t20.5 -46t-20.5 -45t-64.5 -73t-101 -80t-144 -63.5t-181.5 -25.5t-181.5 25.5t-144.5 63.5t-101.5 80t-64.5 73t-20.5 45t20.5 46t64.5 73t101.5 79t144.5 63.5t181.5 25.5zM614.5 293q94.5 0 161 64.5 t66.5 154.5q0 92 -66.5 156.5t-161 64.5t-161 -64.5t-66.5 -156.5q0 -90 66.5 -154.5t161 -64.5zM614 512q8 -8 38 -2t51.5 11t25.5 -9q0 -45 -33.5 -77t-81 -32t-80 32t-32.5 77q0 47 32.5 79t79.5 32q14 0 10.5 -24t-12 -48.5t1.5 -38.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1187" d="M1069 1004q37 -109 -8 -204t-131 -161l18 -25q16 -29 6 -55l-49 -162q-12 -31 -37 -47l-475 -336q-43 -31 -65 4l-215 312q-12 18 -9.5 39.5t21.5 33.5l475 336q27 18 55 19h162q31 0 49 -27l29 -41q172 133 117 293q-10 29 18 41q33 9 39 -20zM848 524q41 33 35 82 l-33 -16q-8 -4 -12 -4q-18 0 -29 18q-12 31 16 41l25 14q-49 35 -94 0q-29 -18 -35 -52t14 -62q18 -27 52 -33t61 12z" />
|
||||
<glyph unicode="" d="M881 659q102 0 173.5 -69.5t71.5 -170t-71.5 -170t-173.5 -69.5h-195v195h109l-181 235l-178 -235h107v-195h-254q-76 0 -131.5 53.5t-55.5 126.5q0 76 54.5 129.5t132.5 53.5q14 0 20 -2q-2 12 -2 39q0 111 80 188.5t193 77.5q92 0 163.5 -53.5t96.5 -137.5q29 4 41 4z " />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M1024 164q-88 156 -213 202t-338 46v-224l-371 342l371 330v-196q92 0 172 -28t134.5 -72t98.5 -97t70.5 -106.5t45 -97.5t24.5 -70z" />
|
||||
<glyph unicode="" d="M473 723l-217 -193l217 -200v-142l-371 342l371 330v-137zM729 664q106 0 186 -51.5t118 -125t61.5 -147.5t27.5 -125l4 -51q-88 158 -172 203t-225 45v-224l-371 342l371 330v-196z" />
|
||||
<glyph unicode="" d="M492 805q16 -14 16 -32.5t-16 -31.5l-252 -229l252 -231q16 -12 16 -31t-16 -33q-31 -31 -62 0l-328 295l328 293q31 31 62 0zM801 805l325 -293l-325 -295q-33 -31 -64 0q-33 33 0 64l254 231l-254 229q-33 31 0 64q31 31 64 0z" />
|
||||
<glyph unicode="" d="M870 215v57l103 84v-192q0 -20 -15.5 -35.5t-35.5 -15.5h-768q-20 0 -36 15t-16 36v563q0 23 14.5 37t37.5 14h295q-33 -25 -61 -50.5t-40 -39.5l-10 -12h-133v-461h665zM786 571q-170 0 -247.5 -42t-163.5 -185q0 8 1 22.5t9 57.5t22.5 81t45 85t71.5 81t109.5 57.5 t152.5 23.5v159l340 -256l-340 -266v182z" />
|
||||
<glyph unicode="" horiz-adv-x="1208" d="M170 692q-27 0 -23 23q4 10 13 14q2 0 50 17.5t95 33t60 15.5h45v153h389v-153h47q12 0 58 -15.5t94.5 -33t50.5 -17.5q18 -8 12 -27q-4 -10 -21 -10h-870zM1051 635q20 0 37.5 -19.5t17.5 -42.5v-178q0 -23 -17.5 -42t-37.5 -19h-103l45 -256h-778l45 256h-100 q-20 0 -39 19.5t-19 41.5v178q0 23 18.5 42.5t39.5 19.5h891zM317 180h574l-72 332h-430z" />
|
||||
<glyph unicode="" d="M358 348h279l131 -143h-459q-43 0 -72.5 30.5t-29.5 71.5v309h-105l181 203l178 -203h-103v-268zM1024 410h102l-178 -205l-180 205h104v266h-280l-131 143h461q41 0 71.5 -29.5t30.5 -72.5v-307z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M819 870q43 0 73 -29.5t30 -72.5v-358q0 -41 -30 -72t-73 -31h-205v-153l-204 153h-205q-41 0 -72 31t-31 72v358q0 43 31 72.5t72 29.5h614z" />
|
||||
<glyph unicode="" d="M399 399h359q2 0 6 2h4v-94q0 -41 -29.5 -71.5t-72.5 -30.5h-256l-154 -154v154h-51q-41 0 -72 30.5t-31 71.5v307q0 43 31 73t72 30h194v-318zM1024 973q43 0 72.5 -30t29.5 -73v-307q0 -41 -29.5 -71.5t-72.5 -30.5h-51v-154l-154 154h-358v409q0 43 30.5 73t71.5 30 h461z" />
|
||||
<glyph unicode="" d="M1024 922q43 0 72.5 -30t29.5 -73v-614q0 -41 -29.5 -72t-72.5 -31h-819q-41 0 -72 31t-31 72v614q0 43 31 73t72 30h819zM1024 205v614h-819v-614h819zM563 406v-93h-256v93h256zM563 559v-92h-256v92h256zM563 713v-92h-256v92h256zM918 385l4 -72h-256q0 72 6 72 q86 23 86 68q0 16 -28 57t-28 90q0 113 92.5 113t92.5 -113q0 -49 -29 -90t-29 -57q0 -20 21.5 -37t44.5 -23z" />
|
||||
<glyph unicode="" d="M539 973q20 0 20 -21v-880q0 -20 -20 -21h-47q-20 0 -21 21v450h-180q-16 0 -29 6q-12 2 -26 13l-123 84q-10 6 -10.5 16t10.5 16l123 84q14 10 26 13q8 4 29 4h180v194q0 20 21 21h47zM1116 760q10 -6 10 -16.5t-10 -16.5l-121 -84q-23 -12 -26 -12q-14 -6 -29 -6h-309 l-41 235h350q18 0 28.5 -4t26.5 -12z" />
|
||||
<glyph unicode="" horiz-adv-x="716" d="M358.5 922q106.5 0 181 -75t74.5 -181q0 -109 -63.5 -249.5t-128.5 -228.5l-64 -86q-10 12 -27.5 36t-61.5 91.5t-77.5 133t-61.5 150.5t-28 153q0 106 75 181t181.5 75zM358.5 524q57.5 0 98.5 41t41 98.5t-41 97.5t-98.5 40t-97.5 -40t-40 -97.5t40 -98.5t97.5 -41z " />
|
||||
<glyph unicode="" d="M1110 768q16 -10 16 -31v-598q0 -20 -16 -30q-8 -6 -16 -7q-8 0 -19 7l-221 139l-221 -139q-18 -10 -35 0l-223 139l-221 -139q-16 -10 -35 0q-16 10 -17 30v598q0 20 17 31l239 150q18 10 35 0l221 -140l224 140q16 10 32 0zM342 307v518l-172 -106v-518zM582 201v518 l-172 106v-518zM821 307v518l-174 -106v-518zM1059 201v518l-172 106v-518z" />
|
||||
<glyph unicode="" horiz-adv-x="1191" d="M590 1004q203 2 348 -139.5t149 -344.5q2 -205 -139 -350t-346 -150q-203 -2 -349.5 140.5t-148.5 345.5q-4 205 138.5 350.5t347.5 147.5zM602 125q160 2 272.5 116.5t110.5 276.5t-117.5 273.5t-275.5 109.5q-162 -2 -273.5 -116.5t-109.5 -276.5t116.5 -273.5 t276.5 -109.5zM362 283q4 27 12.5 67.5t42.5 130.5t79 135.5t128 78t144 42.5l61 11q-4 -27 -12 -68t-42 -131t-79 -135q-43 -43 -127 -76t-145 -43zM547 565q-23 -20 -23 -49t23 -51q20 -23 50 -23t50 23q53 53 90 190q-139 -37 -190 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="983" d="M154 623q125 -72 338 -72t337 72l-55 -498q-2 -14 -35.5 -36.5t-102.5 -44t-144.5 -21.5t-143 21.5t-102.5 44t-37 36.5zM653 930q96 -18 162 -56t66 -73v-10q0 -59 -115 -101.5t-274.5 -42.5t-274.5 42t-115 102v10q0 35 65.5 72.5t162.5 56.5l43 49q23 27 71 27h95 q53 0 71 -27zM598 815h86q-94 113 -106 129q-14 16 -33 17h-105q-23 0 -32 -17l-109 -129h86l66 68h84z" />
|
||||
<glyph unicode="" horiz-adv-x="921" d="M717 973q43 0 72.5 -30t29.5 -73v-716q0 -41 -29.5 -72t-72.5 -31h-512q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h512zM717 154v716h-512v-716h512z" />
|
||||
<glyph unicode="" horiz-adv-x="921" d="M717 973q43 0 72.5 -30t29.5 -73v-716q0 -41 -29.5 -72t-72.5 -31h-512q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h512zM246 760v-90h430v90h-430zM676 268v90h-430v-90h430zM676 469v92h-428v-92h428z" />
|
||||
<glyph unicode="" horiz-adv-x="1230" d="M1096 645q39 -10 30 -47l-153 -569q-4 -16 -18.5 -23.5t-30.5 -3.5l-416 113q-16 4 -24.5 18t-4.5 29l25 94l-185 -49q-41 -10 -51 26l-164 617q-10 37 29 49l465 125q16 4 30.5 -3t18.5 -24l68 -249zM186 840l148 -555l401 108l-147 553zM905 80l135 504l-305 84 l78 -289q10 -35 -29 -47l-200 -53l-27 -105z" />
|
||||
<glyph unicode="" d="M102 768q0 43 31 72.5t72 29.5h819q43 0 72.5 -29.5t29.5 -72.5v-512q0 -41 -29.5 -71.5t-72.5 -30.5h-819q-41 0 -72 30.5t-31 71.5v512zM1024 768h-819v-512h819v512z" />
|
||||
<glyph unicode="" horiz-adv-x="983" d="M102 901q322 0 550.5 -228.5t228.5 -549.5h-121q0 272 -192.5 463.5t-465.5 191.5v123zM102 657q223 0 380 -156.5t157 -377.5h-121q0 170 -122 292t-294 122v120zM219 354q47 0 82 -33.5t35 -82.5q0 -47 -35 -81t-82 -34t-82 33.5t-35 81.5q0 49 35 82.5t82 33.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M768 358q63 0 108.5 -44t45.5 -109q0 -63 -45.5 -108.5t-108.5 -45.5t-108.5 45t-45.5 109q0 6 1 14t1 12l-266 160q-43 -33 -94 -33q-63 0 -108.5 45.5t-45.5 108.5t45.5 108.5t108.5 45.5q55 0 94 -31l266 160q0 4 -1 12t-1 12q0 63 45.5 108.5t108.5 45.5t108.5 -44 t45.5 -110q0 -63 -45.5 -108t-108.5 -45q-53 0 -92 32l-268 -159q2 -8 2 -27q0 -16 -2 -25l268 -159q37 30 92 30z" />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M256 154q0 41 30.5 71.5t71.5 30.5q43 0 73 -30.5t30 -71.5q0 -43 -30 -73t-73 -30q-41 0 -71.5 30t-30.5 73zM768 154q0 41 30.5 71.5t71.5 30.5q43 0 73 -30.5t30 -71.5q0 -43 -30 -73t-73 -30q-41 0 -71.5 30t-30.5 73zM438 395q-37 -10 -34.5 -23.5t45.5 -13.5h575 v-77q0 -20 -20 -21h-134h-512h-24q-20 0 -21 21v77l-10 48l-100 464h-101v82q0 20 21 21h160q20 0 20 -21v-88h721v-280q0 -23 -18 -27z" />
|
||||
<glyph unicode="" d="M451 512q0 70 48 117t115.5 47t115.5 -47t48 -117q0 -68 -48 -116t-115.5 -48t-115.5 48t-48 116zM334 573q-14 -61 -68 -61h-164v123h121q41 127 148.5 207t242.5 80q168 0 291 -119q16 -18 16.5 -44t-16.5 -44q-18 -16 -43.5 -16.5t-44.5 16.5q-80 84 -203 84 q-102 0 -179.5 -64t-100.5 -162zM963 512h163v-123h-120q-41 -127 -147.5 -207t-244.5 -80q-168 0 -288 121q-18 18 -18.5 44t18.5 42q16 18 41.5 18.5t44.5 -18.5q84 -84 202 -84q102 0 180 64.5t101 161.5q13 61 68 61z" />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M922 973q43 0 72.5 -30t29.5 -73v-716q0 -41 -29.5 -72t-72.5 -31h-461q-41 0 -71 31t-30 72v102h101v-102h461v716h-461v-153h-101v153q0 43 30 73t71 30h461zM563 287v123h-461v153h461v123l205 -199z" />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M616 154v102h101v-102q0 -41 -30 -72t-73 -31h-409q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h409q43 0 73 -30t30 -73v-153h-101v153h-411v-716h411zM1024 487l-203 -200v123h-461v153h461v123z" />
|
||||
<glyph unicode="" horiz-adv-x="1015" d="M590 918h323v-324l-102 127l-149 -156l-103 103l156 149zM354 463l103 -103l-156 -149l125 -102h-324v323l103 -125z" />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M262 303l-108 103h303v-304l-103 109l-149 -160l-103 103zM1024 870l-158 -147l107 -102h-301v301l102 -107l148 158z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M819 922q43 0 73 -30t30 -73v-409q0 -41 -30 -72t-73 -31h-409q-41 0 -72 31t-31 72v411q0 41 30 71t73 30h409zM819 410v409h-409v-409h409zM205 512v-307h307v-103h-307q-41 0 -72 31t-31 72v307h103z" />
|
||||
<glyph unicode="" d="M1024 973q43 0 72.5 -31t29.5 -72v-614q0 -43 -29.5 -72.5t-72.5 -29.5h-203v100h205v473h-821v-473h205v-100h-205q-41 0 -72 29.5t-31 72.5v614q0 41 31 72t72 31h819zM236 801q39 0 38 39q0 16 -11 26.5t-27.5 10.5t-27.5 -11.5t-11 -25.5q0 -16 11.5 -27.5 t27.5 -11.5zM338 801q39 0 39 39q0 16 -11.5 26.5t-27.5 10.5t-27.5 -11.5t-11.5 -25.5q0 -16 11.5 -27.5t27.5 -11.5zM1026 807v63h-616v-63h616zM612 604l248 -246h-153v-307h-189v307h-153z" />
|
||||
<glyph unicode="" d="M1024 922q43 0 72.5 -31t29.5 -72v-614q0 -43 -29.5 -73t-72.5 -30h-819q-41 0 -72 30t-31 73v614q0 41 31 72t72 31h819zM338 825q-16 0 -27.5 -11t-11.5 -26q0 -16 11.5 -27t27.5 -11q39 0 39 38q0 16 -11.5 26.5t-27.5 10.5zM197 788q0 -16 11 -27t28 -11q39 0 38 38 q0 16 -11 26.5t-27.5 10.5t-27.5 -11.5t-11 -25.5zM1026 205v471h-821v-471h821zM1026 758v61h-616v-61h616z" />
|
||||
<glyph unicode="" horiz-adv-x="675" d="M338 1024l235 -373h-471zM338 0l-236 375h471z" />
|
||||
<glyph unicode="" horiz-adv-x="1146" d="M573.5 983q194.5 0 332.5 -138t138 -333t-138 -333t-332.5 -138t-333 138t-138.5 333t138.5 333t333 138zM573.5 143q151.5 0 260 108.5t108.5 260.5q0 154 -108.5 261.5t-260 107.5t-260 -107.5t-108.5 -261.5q0 -152 108.5 -260.5t260 -108.5zM666 711v-211h114 l-207 -195l-206 195h114v211h185z" />
|
||||
<glyph unicode="" horiz-adv-x="1146" d="M1044 512q0 -195 -138 -333t-332.5 -138t-333 138t-138.5 333q0 197 138.5 334t333 137t332.5 -137t138 -334zM205 512q0 -152 108.5 -260.5t260 -108.5t260 108.5t108.5 260.5q0 154 -108.5 261.5t-260 107.5t-260 -107.5t-108.5 -261.5zM770 420h-209v-115l-194 207 l194 209v-117h209v-184z" />
|
||||
<glyph unicode="" horiz-adv-x="1146" d="M102 512q0 195 138.5 333t333 138t332.5 -138t138 -333t-138 -333t-332.5 -138t-333 138t-138.5 333zM942 512q0 154 -107.5 261.5t-261.5 107.5q-152 0 -260 -107.5t-108 -261.5q0 -152 108.5 -260.5t259.5 -108.5q154 0 261.5 108.5t107.5 260.5zM377 604h209v117 l194 -209l-194 -207v115h-209v184z" />
|
||||
<glyph unicode="" horiz-adv-x="1146" d="M573.5 41q-194.5 0 -333 138t-138.5 333q0 197 138.5 334t333 137t332.5 -137t138 -334q0 -195 -138 -333t-332.5 -138zM573 881q-152 0 -260 -107.5t-108 -261.5q0 -152 108.5 -260.5t259.5 -108.5q154 0 261.5 108.5t107.5 260.5q0 154 -107.5 261.5t-261.5 107.5z M481 315v209h-114l206 197l207 -197h-114v-209h-185z" />
|
||||
<glyph unicode="" horiz-adv-x="798" d="M680 586l-240 -230q-18 -18 -40.5 -18t-41.5 18l-239 230q-16 16 -16.5 41.5t16.5 42.5q39 39 80 0l200 -193l201 193q41 39 80 0q16 -16 16 -42t-16 -42z" />
|
||||
<glyph unicode="" horiz-adv-x="552" d="M350 795q14 16 40 16t42 -16q39 -37 0 -82l-190 -201l190 -199q39 -45 0 -82q-16 -16 -40.5 -16t-41.5 16l-231 242q-16 16 -17 39q0 25 17 41q211 219 231 242z" />
|
||||
<glyph unicode="" horiz-adv-x="552" d="M203 795l231 -242q16 -16 17 -41q0 -23 -17 -39l-231 -242q-16 -16 -41 -16t-41 16q-37 37 0 82l190 199l-190 201q-37 45 0 82q16 16 42 16t40 -16z" />
|
||||
<glyph unicode="" horiz-adv-x="798" d="M680 440q16 -16 16 -41.5t-16 -42.5q-39 -39 -80 0l-201 193l-200 -193q-41 -39 -80 0q-16 16 -16.5 42t16.5 42l239 230q16 16 41 16t41 -16z" />
|
||||
<glyph unicode="" horiz-adv-x="679" d="M516 635q23 27 49 0q27 -23 0 -49l-200 -197q-23 -23 -50 0l-200 197q-27 27 0 49q25 25 51 0l174 -160z" />
|
||||
<glyph unicode="" horiz-adv-x="475" d="M360 338q27 -27 0 -49q-27 -27 -49 0l-196 198q-25 25 0 52l196 198q23 27 49 0q27 -23 0 -49l-159 -176z" />
|
||||
<glyph unicode="" horiz-adv-x="475" d="M115 338l161 174l-161 176q-27 27 0 49q27 27 49 0l196 -198q25 -27 0 -52l-196 -198q-23 -27 -49 0q-27 22 0 49z" />
|
||||
<glyph unicode="" horiz-adv-x="679" d="M166 389q-27 -23 -51 0q-25 25 0 51l200 195q27 27 50 0l200 -195q25 -27 0 -51q-25 -23 -51 0l-174 162z" />
|
||||
<glyph unicode="" horiz-adv-x="1089" d="M166 737l379 -364l381 364q23 27 49 0q27 -23 0 -49l-406 -401q-23 -23 -49 0l-405 401q-27 27 0 49q24 25 51 0z" />
|
||||
<glyph unicode="" horiz-adv-x="679" d="M565 133q27 -27 0 -49q-27 -27 -49 0l-401 403q-25 25 0 52l401 403q23 27 49 0q27 -23 0 -49l-366 -381z" />
|
||||
<glyph unicode="" horiz-adv-x="679" d="M115 133l366 379l-366 381q-27 27 0 49q27 27 49 0l401 -403q25 -27 0 -52l-401 -403q-23 -27 -49 0q-27 22 0 49z" />
|
||||
<glyph unicode="" horiz-adv-x="1089" d="M926 287l-381 366l-379 -366q-27 -23 -51 0q-25 25 0 51l405 399q27 27 49 0l406 -399q25 -27 0 -51q-26 -23 -49 0z" />
|
||||
<glyph unicode="" d="M1126 614v-256q0 -43 -29.5 -72.5t-72.5 -29.5h-819q-41 0 -72 29.5t-31 72.5v308q0 41 31 71.5t72 30.5h819q43 0 72.5 -30.5t29.5 -71.5v-52zM1024 358v308h-819v-308h819z" />
|
||||
<glyph unicode="" d="M1126 614v-256q0 -43 -29.5 -72.5t-72.5 -29.5h-819q-41 0 -72 29.5t-31 72.5v308q0 41 31 71.5t72 30.5h819q43 0 72.5 -30.5t29.5 -71.5v-52zM1024 358v308h-819v-308h819zM256 410v202h205v-202h-205z" />
|
||||
<glyph unicode="" d="M1126 614v-256q0 -43 -29.5 -72.5t-72.5 -29.5h-819q-41 0 -72 29.5t-31 72.5v308q0 41 31 71.5t72 30.5h819q43 0 72.5 -30.5t29.5 -71.5v-52zM1024 358v308h-819v-308h819zM256 410v202h205v-202h-205zM512 410v202h205v-202h-205z" />
|
||||
<glyph unicode="" d="M1126 614v-256q0 -43 -29.5 -72.5t-72.5 -29.5h-819q-41 0 -72 29.5t-31 72.5v308q0 41 31 71.5t72 30.5h819q43 0 72.5 -30.5t29.5 -71.5v-52zM1024 358v308h-819v-308h819zM256 410v202h205v-202h-205zM512 410v202h205v-202h-205zM768 612h205v-202h-205v202z" />
|
||||
<glyph unicode="" horiz-adv-x="1167" d="M647 932q174 0 296 -123t122 -297t-122 -297t-296 -123q-141 0 -258 90l72 78q84 -61 186 -61q129 0 221.5 92t92.5 221q0 131 -92.5 223t-221.5 92q-127 0 -218 -88t-95 -215h145l-188 -211l-189 211h127q4 170 126 289t292 119zM610 737h72v-209l133 -133l-51 -51 l-154 154v239z" />
|
||||
<glyph unicode="" horiz-adv-x="1146" d="M332 670q-23 0 -43 -11q-49 53 -94 132q55 76 131 120q94 -39 155 -84q-6 -16 -6 -32q0 -6 4 -23q-63 -49 -119 -106q-16 4 -28 4zM231 569q0 -35 21 -61q-61 -117 -82 -238q-68 109 -68 242q0 113 52 211q39 -63 86 -115q-9 -25 -9 -39zM575 895q-29 0 -51 -14 q-59 43 -116 71q86 31 165 31q123 0 236 -63q-78 -14 -166 -52q-27 27 -68 27zM725 424q-164 25 -297 115q4 20 4 30q0 25 -14 54q39 45 100 92q27 -20 57 -21q14 0 39 8q96 -111 138 -245q-17 -15 -27 -33zM856 289q39 14 55 61q59 4 111 19q-47 -150 -170 -238q6 49 6 100 q0 10 -1 29t-1 29zM702 356q-195 -98 -311 -278q-92 37 -162 112q12 147 82 281q6 -2 21 -2q31 0 53 14q145 -100 317 -127zM891 860q154 -139 153 -348q0 -23 -4 -68q-66 -18 -133 -24q-25 57 -90 61q-49 150 -151 271q10 20 10 43v10q102 43 215 55zM752 303 q14 -10 32 -18q2 -18 2 -54q0 -82 -14 -147q-88 -43 -199 -43q-59 0 -114 12q113 164 293 250z" />
|
||||
<glyph unicode="" horiz-adv-x="1230" d="M1094 561q41 -43 30 -74l-28 -157q-4 -20 -22.5 -33.5t-41.5 -13.5h-835q-23 0 -41.5 13t-22.5 34l-29 157q-8 33 33 74q8 10 37 39t70 69t53 52q23 23 53 22h264h265q31 0 53 -22q16 -16 54 -53t69 -67t39 -40zM821 528h183l-105 117h-569l-105 -117h183q8 0 12 -8 l41 -102h307l41 102q4 8 12 8z" />
|
||||
<glyph unicode="" horiz-adv-x="1128" d="M1008 467q25 -53 14 -98l-35 -189q-2 -20 -19.5 -35.5t-39.5 -15.5h-729q-23 0 -40.5 15.5t-19.5 35.5l-35 189q-8 51 15 98l162 383q23 47 73 47h107l-21 -209h-137l260 -215l262 215h-139l-18 209h104q51 0 76 -47zM938 332q2 23 -10.5 39t-34.5 16h-660 q-23 0 -35 -16.5t-10 -38.5l15 -76q2 -23 19.5 -38t37.5 -15h606q23 0 40.5 15t19.5 38z" />
|
||||
<glyph unicode="" horiz-adv-x="1150" d="M569.5 983q194.5 2 333.5 -133t144 -330q2 -195 -134.5 -335t-330.5 -144q-195 -2 -335.5 134t-142.5 331q-4 195 133.5 335t332 142zM569 922q-96 0 -182 -45l64 -107q57 29 124.5 29t124.5 -29l64 107q-91 47 -195 45zM317 387q-29 61 -28 125q0 66 28 127l-104 63 q-47 -90 -47 -194q2 -98 47 -184zM582 102q100 4 182 48l-64 106q-61 -31 -124.5 -31t-124.5 31l-64 -106q89 -48 195 -48zM575.5 287q94.5 0 160 66.5t65.5 158.5q0 94 -65.5 159.5t-160 65.5t-160 -65.5t-65.5 -159.5q0 -92 65.5 -158.5t160 -66.5zM834 387l106 -63 q47 98 45 194q0 98 -45 184l-106 -63q29 -61 28 -127q1 -64 -28 -125z" />
|
||||
<glyph unicode="" horiz-adv-x="780" d="M666 287q29 -82 -17.5 -161t-142.5 -114q-96 -29 -179 9t-106 120l-108 394q-20 70 6 137t86 108l-99 191q-14 35 15 49q31 18 49 -14l100 -197q82 23 158 -16t104 -119zM334 567q29 10 41 37t4 55q-10 29 -36 42.5t-54 5.5q-29 -10 -41 -37t-4 -56q10 -29 35.5 -42 t54.5 -5z" />
|
||||
<glyph unicode="" horiz-adv-x="1189" d="M223 328q39 35 87 29.5t89 -46.5q43 -41 49.5 -89t-30.5 -85q-88 -86 -234 -104q-86 -12 -82 14q0 4 7 10q53 61 65 148.5t49 122.5zM1083 989q27 -27 -151 -254t-299 -346q-39 -39 -127 -106q-8 -6 -17 8q-18 35 -49 65q-33 33 -67 50q-16 6 -8 16q66 86 106 125 q121 119 352 294t260 148z" />
|
||||
<glyph unicode="" horiz-adv-x="1044" d="M633 109l16 167l266 -20l-16 -170q-4 -29 -33 -25l-204 17q-29 0 -29 31zM131 256l264 20l17 -167q2 -12 -6.5 -21.5t-22.5 -9.5l-203 -17q-12 -2 -22.5 6.5t-10.5 18.5zM104 532q-2 12 -2 35q0 164 123 280t297 116t297 -116t123 -280q0 -23 -2 -35l-16 -174l-265 23 l17 174v12q0 59 -45 101.5t-108.5 42.5t-108.5 -42t-45 -102v-12l16 -174l-264 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1208" d="M961 768v-51h-713v51q0 23 13 36t26 15h12h610q6 0 14.5 -1t23 -14t14.5 -36zM809 922q6 0 14 -1t22.5 -14.5t14.5 -36.5h-510q0 23 13.5 36.5t25.5 15.5h12h408zM1063 717q35 -33 39 -47q6 -18 0 -56l-78 -460q-4 -23 -20.5 -36.5t-28.5 -15.5h-14h-713q-53 0 -62 52 q-6 27 -39.5 228.5t-40.5 231.5q-10 23 -2.5 45.5t10.5 26.5t21 21l10 10l31 31v-82h856v82zM809 440v103h-72v-82h-266v82h-70v-103q0 -51 50 -51h307q23 0 36 12.5t13 24.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" />
|
||||
<glyph unicode="" horiz-adv-x="1126" d="M973 819q20 0 35.5 -15.5t15.5 -35.5v-205h-512v256h461zM102 256v205h308v-256h-267q-41 0 -41 51zM512 205v256h512v-205q0 -23 -14.5 -37t-36.5 -14h-461zM102 768q0 51 41 51h267v-256h-308v205z" />
|
||||
<glyph unicode="🌄" d="M979 684h-102l-127 154l-220 -154h-184q-53 0 -92 -40t-39 -93v-164l-111 303q-10 39 23 53l696 254q37 10 51 -24zM1087 592q16 0 27.5 -12.5t11.5 -28.5v-483q0 -16 -11 -28.5t-28 -12.5h-741q-16 0 -27.5 12t-11.5 29v483q0 16 11.5 28.5t27.5 12.5h741zM1030 129v166 l-74 164l-170 -62l-133 -135l-141 174l-94 -219v-88h612z" />
|
||||
<glyph unicode="🌎" horiz-adv-x="1187" d="M594 1004q205 0 348 -144.5t143 -347.5q0 -205 -143 -348.5t-348 -143.5q-203 0 -347.5 143.5t-144.5 348.5q0 203 144.5 347.5t347.5 144.5zM1014 512q0 135 -80 244.5t-207 152.5q-18 -25 -16 -32q4 -39 18 -52.5t30.5 -7.5t33 12.5t20.5 1.5q23 -25 0.5 -48t-46.5 -57 t-1 -79q35 -66 98 -65q29 -2 44.5 -37t17.5 -68q10 -82 -15 -143q-23 -45 15 -78q88 115 88 256zM537 926q-115 -14 -204 -86t-130 -178q6 0 22.5 -2.5t28.5 -3.5t26.5 -4t24.5 -8t12 -13q4 -12 -14 -46t-18 -63q0 -31 38.5 -57.5t38.5 -46.5q0 -29 8.5 -70t8.5 -45 q0 -12 37 -55t53 -43q10 0 11 22.5t-2 55t-3 41.5q0 33 14 75q12 43 60.5 72t56.5 47q16 35 9 62.5t-17 44t-34.5 29t-42 17.5t-38 9t-22.5 4q-16 6 -43 7t-37 -3t-27.5 11.5t-17.5 29.5q0 10 15.5 27.5t36 38t28.5 30.5q8 14 17 21.5t22.5 16.5t27.5 22q4 4 26 17.5 t28 23.5zM463 113q68 -20 131 -21q131 0 231 70q-27 45 -120 35q-25 -2 -67 -17.5t-48 -17.5q-76 -16 -78 -17q-12 -2 -26.5 -14t-22.5 -18z" />
|
||||
<glyph unicode="🍂" horiz-adv-x="1167" d="M344 815q186 109 518 68q172 -23 201 -52q4 -6 -2 -10q-78 -41 -133.5 -111.5t-80 -135t-66.5 -135t-95 -107.5q-141 -98 -391 -4q-68 -78 -117 -181q-12 -25 -48 -7t-26 40q45 102 132.5 197.5t180.5 157t180 108.5t144 70l56 20q-14 0 -42 -1t-107 -14t-151.5 -39 t-165.5 -86.5t-165 -143.5q-23 247 178 366z" />
|
||||
<glyph unicode="🎓" d="M272 397l342 -172l283 140q-4 -23 -8 -48.5t-6 -36t-11.5 -23.5t-25 -23.5t-45.5 -22.5q-41 -18 -82 -42t-64.5 -35t-40 -11t-41 13.5t-65.5 38t-82 40.5q-74 33 -106 70.5t-48 111.5zM1102 649q25 -14 24.5 -33.5t-24.5 -33.5l-80 -45l-315 104q-23 37 -93 37 q-41 0 -68.5 -16.5t-27.5 -41t27.5 -41t68.5 -16.5q27 0 37 4l299 -69l-274 -156q-61 -33 -123 0l-426 240q-25 14 -25 33.5t25 33.5l426 240q61 33 123 0zM971 197q18 119 13 186.5t-19 91.5l-15 23l72 39q6 -8 12 -29t17.5 -103.5t-7.5 -201.5q-4 -27 -22 -31t-35.5 5.5 t-15.5 19.5z" />
|
||||
<glyph unicode="🎔" horiz-adv-x="1230" d="M1114 467q31 -84 -10 -180.5t-137 -163.5q-10 0 -12.5 2t-16.5 19.5t-16 19.5q-2 6 2 10q88 61 119.5 155.5t-11.5 151.5q-16 -39 -39.5 -77.5t-60.5 -81.5t-88 -67t-109 -15q-53 6 -86 41.5t-33 95.5q0 86 62 151q51 51 117 68l-2 102q-143 -25 -150 -24q-6 -2 -10 4 q0 2 -5 29.5t-5 31.5q-2 2 1 4t7 2l160 29q0 113 -3 117q0 8 9 8q47 0 53 2q10 0 10 -8v-107q162 23 168 23q8 4 10 -6q0 -2 4.5 -23.5t4.5 -25.5q4 -10 -5 -13l-180 -30v-105h12q88 0 151.5 -36.5t88.5 -102.5zM735 303q29 -6 64 6l-4 219q-35 -12 -62 -41 q-45 -45 -45 -110q0 -68 47 -74zM860 332q29 25 59.5 69.5t46 80.5t7.5 42q-37 18 -99 19q-2 0 -6 -1t-6 -1zM401 723q10 -29 54.5 -169t85.5 -267t41 -129q0 -4 -4 -4h-89q-6 0 -6 4l-51 170h-180q-49 -168 -51 -170q0 -4 -6 -4h-89q-4 0 -4 4q10 18 181 565q2 8 10 8h98 q10 0 10 -8zM268 399h148l-74 271z" />
|
||||
<glyph unicode="🎤" horiz-adv-x="860" d="M737 653q20 0 21 -20v-141q0 -94 -71 -168t-206 -86v-136h133q20 0 21 -20v-62q0 -20 -21 -20h-368q-20 0 -21 20v62q0 20 21 20h133v136q-135 12 -206 85.5t-71 168.5v141q0 20 21 20h31q20 0 20 -20v-141q0 -68 60.5 -126.5t195.5 -58.5t195.5 58.5t60.5 126.5v141 q0 20 21 20h30zM430 410q-82 0 -118 25.5t-36 56.5v161h308v-161q0 -31 -36 -56.5t-118 -25.5zM584 942v-217h-308v217q0 31 36 56.5t118 25.5t118 -25.5t36 -56.5z" />
|
||||
<glyph unicode="🎨" horiz-adv-x="1210" d="M981 791q74 -49 103.5 -113t20.5 -107t-36 -49q-16 -4 -55 10.5t-82 10.5t-82 -47q-31 -47 -21.5 -77t35 -67t23.5 -51q-2 -27 -37 -64.5t-129 -75.5t-221 -38q-190 0 -298 103.5t-98 250.5q8 121 106.5 241t221.5 154q297 87 549 -81zM655 313q31 0 53.5 22.5t22.5 55.5 t-22.5 54.5t-53.5 21.5q-33 0 -55 -21.5t-22 -54.5t22 -55.5t55 -22.5z" />
|
||||
<glyph unicode="🎫" horiz-adv-x="1169" d="M324 432l333 334l183 -182l-334 -334zM1051 682q14 -14 14 -36.5t-14 -37.5l-564 -563q-16 -16 -36.5 -16t-36.5 16l-78 78q12 20 12 49q0 43 -29.5 73.5t-72.5 30.5q-23 0 -51 -14l-76 78q-16 16 -16.5 36.5t16.5 37.5l563 563q14 14 36.5 14t37.5 -14l75 -78 q-12 -23 -12 -49q0 -43 31 -72.5t74 -29.5q27 0 49 12zM506 168l416 416l-265 264l-417 -416z" />
|
||||
<glyph unicode="🎬" horiz-adv-x="1208" d="M1106 768h-102v-102h102v-103h-102v-102h102v-103h-102v-102h102v-61q0 -16 -12.5 -28.5t-28.5 -12.5h-922q-16 0 -28.5 12t-12.5 29v61h103v102h-103v103h103v102h-103v103h103v102h-103v61q0 18 12.5 29.5t28.5 11.5h922q16 0 28.5 -11t12.5 -30v-61zM492 358l256 154 l-256 154v-308z" />
|
||||
<glyph unicode="🎯" horiz-adv-x="1085" d="M542.5 952q182.5 0 311.5 -129t129 -311t-129 -311t-311.5 -129t-311.5 129t-129 311t129 311t311.5 129zM580 156q127 14 217 104t102 217h-197v72h197q-12 127 -102 217t-217 104v-198h-72v198q-127 -14 -218 -104t-104 -217h199v-72h-199q12 -127 103.5 -217 t218.5 -104v198h72v-198z" />
|
||||
<glyph unicode="🎵" horiz-adv-x="921" d="M717 973q43 0 72.5 -30t29.5 -73v-716q0 -41 -29.5 -72t-72.5 -31h-512q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h512zM604 485q33 45 23.5 94.5t-34 82t-50 67.5t-25.5 55h-61v-376q-43 16 -92 -2q-43 -14 -66 -49t-12 -66q12 -33 54 -46.5t87 1.5q90 31 90 100v268 q37 -6 56.5 -32.5t18.5 -53t-3 -43.5q-4 -10 2 -10q4 0 12 10z" />
|
||||
<glyph unicode="🏆" horiz-adv-x="1126" d="M625 291v-68q72 -8 119 -32.5t47 -57.5q0 -37 -67 -64.5t-161 -27.5q-92 0 -159.5 27.5t-67.5 64.5q0 33 47 57.5t121 32.5v68q0 51 -34 86t-116 90q-57 37 -89 62.5t-77 73.5t-65.5 110.5t-20.5 140.5q0 14 11.5 24.5t25.5 10.5h176q49 94 248 94q201 0 250 -94h174 q14 0 25.5 -10.5t11.5 -24.5q0 -78 -20.5 -140.5t-65.5 -110.5t-77 -73.5t-89 -62.5q-80 -53 -113.5 -89t-33.5 -87zM766 549q82 57 129 116.5t55 151.5h-129q-6 -162 -55 -268zM563.5 922q-63.5 0 -110.5 -15.5t-65.5 -33t-18.5 -29.5q0 -14 18.5 -31.5t65.5 -33 t110.5 -15.5t110.5 15.5t65.5 32.5t18.5 32q0 12 -18.5 29.5t-65.5 33t-110.5 15.5zM176 817q8 -92 55.5 -151.5t128.5 -116.5q-49 106 -55 268h-129z" />
|
||||
<glyph unicode="👍" horiz-adv-x="1024" d="M698 645q2 -6 59.5 -13t111 -24.5t53.5 -48.5q0 -74 -62.5 -291t-109.5 -217q-147 0 -295 43t-148 90v351q0 14 15.5 34.5t47 46t54 42t63.5 44t48 31.5q51 35 106 102.5t87 106.5t42 27q49 -78 29.5 -140.5t-60.5 -122t-41 -61.5zM256 641q14 0 0 -14q-51 -51 -51 -107 v-325q0 -51 53 -107q10 -10 -2 -10q-27 0 -56.5 8t-63.5 46t-34 101v248q0 63 34 102.5t64.5 48.5t55.5 9z" />
|
||||
<glyph unicode="👎" horiz-adv-x="1024" d="M326 377q-2 6 -58.5 13t-111 24.5t-54.5 48.5q0 74 63.5 292t108.5 218q147 0 295 -44t148 -91v-351q0 -10 -8.5 -24t-25.5 -30.5t-32.5 -30t-43 -33t-42 -29.5t-42 -28.5t-34.5 -22.5q-51 -35 -106 -102.5t-87 -106.5t-42 -27q-49 78 -29.5 140.5t60.5 122t41 61.5z M768 381q-12 0 2 14q49 51 49 107v325q0 51 -53 107q-10 10 2 10q27 0 56.5 -8t63.5 -46t34 -102v-247q0 -49 -18.5 -83t-46.5 -49.5t-49.5 -21.5t-39.5 -6z" />
|
||||
<glyph unicode="👜" horiz-adv-x="1087" d="M958 838q29 -27 25 -62l-100 -663q-8 -31 -39 -31h-600q-29 0 -41 31q-96 635 -99 663q-4 35 23 62q6 6 55 44t58 44q18 16 57 16h491q39 0 58 -16q79 -59 112 -88zM543 391q57 0 100 35t64.5 91t31 91t13.5 68h-95q-39 -193 -114.5 -193t-114.5 193h-94 q47 -285 209 -285zM182 768h721l-112 119h-496z" />
|
||||
<glyph unicode="👤" horiz-adv-x="1167" d="M856 285q209 -74 209 -125v-109h-481h-482v109q0 51 209 125q96 35 131 70.5t35 97.5q0 23 -22.5 50t-32.5 75q-2 12 -9 18t-14.5 8t-14.5 17.5t-9 44.5q0 16 5 26t9 13l4 4q-8 51 -12 90q-4 55 42 114.5t160.5 59.5t162 -59.5t40.5 -114.5l-12 -90q18 -8 19 -43 q-2 -29 -9.5 -44.5t-14.5 -17.5t-14 -8t-10 -18q-8 -49 -31.5 -76t-23.5 -49q0 -61 36 -97t130 -71z" />
|
||||
<glyph unicode="👥" d="M1126 61h-229v154q0 55 -30.5 83t-157.5 91q41 31 41 86q0 16 -13.5 33.5t-19.5 52.5q-2 8 -14.5 16.5t-14.5 43.5q0 25 12 30q-6 35 -8 62q-4 39 23.5 80t97.5 41t98.5 -41t24.5 -80l-8 -62q12 -6 12 -30q-2 -35 -14.5 -43.5t-14.5 -16.5q-6 -35 -19 -52t-13 -34 q0 -43 21.5 -67.5t78.5 -49.5q115 -47 133 -82q6 -8 9 -62t5 -103v-50zM627 330q186 -80 186 -127v-142h-711v189q0 45 86 80q78 33 107 65.5t29 89.5q0 20 -19.5 45t-25.5 70q-2 10 -18.5 22.5t-20.5 57.5q0 14 3 23.5t7 13.5l4 2q-6 47 -10 84q-4 51 33.5 105.5t130 54.5 t130 -54.5t33.5 -105.5l-10 -84q14 -8 14 -39q-4 -45 -20 -57.5t-18 -22.5q-6 -45 -25.5 -69.5t-19.5 -45.5q0 -57 28.5 -89.5t106.5 -65.5z" />
|
||||
<glyph unicode="💡" horiz-adv-x="923" d="M317 41v106h289v-106q-72 -43 -145 -41q-72 -2 -144 41zM600 209h-276q0 74 -37 143.5t-80 115.5t-76 114.5t-27 142.5q8 123 96.5 211t260.5 88q174 0 261 -88t97 -211q4 -61 -16.5 -115.5t-53 -98.5t-66.5 -87t-58.5 -98.5t-24.5 -116.5zM213 717q-4 -4 0 -20.5 t2 -20.5t5 -19.5t6 -18.5t8.5 -18.5t11.5 -19.5t13 -19.5t14 -19.5t15.5 -21.5t16.5 -23.5q90 -125 115 -217h84q25 96 114 217q4 6 26 36t26 37t17 29.5t16.5 34t6.5 28.5t1 36q-16 201 -250 201q-232 0 -248 -201z" />
|
||||
<glyph unicode="💥" horiz-adv-x="1130" d="M1010 393q20 -16 16 -33.5t-29 -23.5l-79 -23q-25 -6 -41.5 -28.5t-14.5 -48.5l4 -84q2 -25 -14 -35.5t-39 0.5l-88 45q-23 12 -48.5 4t-35.5 -31l-47 -90q-12 -23 -29.5 -24t-34.5 20l-51 80q-35 49 -90 20l-125 -71q-23 -14 -33 -6t-2 32l56 168q8 25 -4.5 45.5 t-36.5 22.5l-109 12q-25 4 -30 18.5t16 30.5l88 78q20 16 20 42t-20 42l-88 78q-20 16 -16 33.5t28 23.5l80 23q25 6 42 28.5t15 49.5l-6 83q0 27 15.5 37.5t37.5 -0.5l82 -39q25 -10 50.5 -1.5t37.5 30.5l48 82q12 23 30.5 22t30.5 -24l51 -88q12 -23 36 -30t46 7l139 86 q23 14 31 6t0 -32l-61 -174q-10 -23 2 -42.5t39 -21.5l116 -12q27 -2 31 -16.5t-16 -30.5l-88 -78q-18 -16 -18.5 -42t18.5 -42zM616 299v107h-102v-107h102zM616 463v266h-102v-266h102z" />
|
||||
<glyph unicode="💦" horiz-adv-x="1167" d="M274 1018q10 -88 51.5 -159t75 -126t33.5 -115q0 -68 -49 -115.5t-116.5 -47.5t-117 48t-49.5 115q0 59 34 114.5t75 126.5t51 159q2 4 7 4t5 -4zM905 1018q10 -88 51 -159t75 -126t34 -115q0 -68 -49 -115.5t-117 -47.5t-117 48t-49 115q0 49 21.5 95.5t49 80t54.5 94 t35 130.5q2 4 7 4t5 -4zM578 563q2 4 7 4t5 -4q10 -88 51 -158.5t75 -126t34 -114.5q0 -68 -49.5 -116t-117 -48t-116.5 48t-49 116q0 59 33.5 114.5t75 126t51.5 158.5z" />
|
||||
<glyph unicode="💧" horiz-adv-x="778" d="M399 995q14 -121 61.5 -224t94.5 -162.5t84 -139.5t37 -164q0 -117 -85 -201t-202 -84t-202 84t-85 201q0 84 37 164t84 139.5t94.5 162.5t61.5 224q2 8 11 8t9 -8zM356 594q2 4 -2 14q-6 6 -14 6t-12 -6l-41 -59q-33 -47 -49.5 -71.5t-35 -77t-18.5 -103.5 q0 -25 17.5 -42t42.5 -17q59 0 59 69q0 96 43 252q2 6 5 17.5t5 17.5z" />
|
||||
<glyph unicode="💨" horiz-adv-x="1130" d="M188 700q-16 -14 -36.5 -12t-34.5 19q-14 14 -12.5 36.5t18.5 36.5q49 41 81 61.5t91 41t132 4t163 -67.5t158.5 -54t102.5 16.5t91 68.5q39 31 72 -6q33 -41 -6 -74q-125 -113 -240 -113q-102 0 -227 72q-70 39 -122 53.5t-95 0t-67 -30t-69 -52.5zM942 588q39 33 72 -6 q33 -41 -6 -74q-41 -35 -67 -54.5t-74 -39t-99 -19.5q-98 0 -227 72q-70 39 -122 53.5t-95 0t-67 -30t-69 -52.5q-14 -14 -35.5 -12t-35.5 18q-33 41 6 74q39 35 61.5 51.5t70.5 39t90 23.5t107.5 -15.5t137.5 -57.5q70 -39 122 -53t95 0t66.5 29.5t68.5 52.5zM942 326 q39 33 72 -7q14 -14 12 -36.5t-18 -36.5q-41 -35 -67 -54.5t-74 -39t-99 -19.5q-98 0 -227 72q-70 39 -122 53t-95 1t-68 -29.5t-68 -53.5q-14 -14 -35.5 -12t-35.5 18q-33 41 6 74q39 35 61.5 51.5t70.5 39t90 23.5t107.5 -15.5t137.5 -57.5q70 -39 122 -53.5t95 0t66.5 30 t68.5 52.5z" />
|
||||
<glyph unicode="💳" d="M1024 870q43 0 72.5 -30.5t29.5 -71.5v-512q0 -43 -29.5 -72.5t-72.5 -29.5h-819q-41 0 -72 29.5t-31 72.5v512q0 41 31 71.5t72 30.5h819zM1024 256v307h-819v-307h819zM1024 717v51h-819v-51h819zM307 455h31v-31h-31v31zM492 393h30v31h31v31h61v-31h-30v-31h-31v-31 h-61v31zM614 362h-30v31h30v-31zM461 362h-62v31h62v-31zM492 424v-31h-31v62h61v-31h-30zM369 393v-31h-62v31h31v31h31v31h61v-31h-31v-31h-30z" />
|
||||
<glyph unicode="💻" d="M1024 963q43 0 72.5 -31t29.5 -72v-563q0 -43 -29.5 -79t-70.5 -44l-223 -45l88 -39q51 -29 -21 -29h-512q-100 0 33 54l37 14l-225 45q-41 8 -71 44t-30 79v563q0 41 31 72t72 31h819zM1024 301v569h-819v-569h819z" />
|
||||
<glyph unicode="💼" d="M569 487v-102h-467q8 231 11 299q4 111 102 111h164q16 27 37.5 68.5t23.5 45.5q14 27 23.5 33t38.5 6h227q27 0 37 -7t22 -32q18 -33 62 -114h164q98 0 102 -111l10 -299h-464v102h-93zM494 850l-29 -55h299l-29 55q-14 27 -43 27h-155q-29 0 -43 -27zM662 231v103h440 q-6 -90 -10 -170q-6 -86 -93 -86h-768q-92 0 -92 86l-10 170h440v-103h93z" />
|
||||
<glyph unicode="💾" horiz-adv-x="1024" d="M776 922l146 -160v-557q0 -41 -30 -72t-73 -31h-614q-41 0 -72 31t-31 72v614q0 43 31 73t72 30h571zM717 614v256h-410v-256q0 -20 15.5 -35.5t35.5 -15.5h308q20 0 35.5 15.5t15.5 35.5zM666 819v-205h-103v205h103z" />
|
||||
<glyph unicode="💿" horiz-adv-x="1146" d="M573.5 983q194.5 0 332.5 -138t138 -333t-138 -333t-332.5 -138t-333 138t-138.5 333t138.5 333t333 138zM573.5 358q63.5 0 108.5 45.5t45 108.5q0 66 -44 110t-110 44q-63 0 -108 -45.5t-45 -108.5t45 -108.5t108.5 -45.5z" />
|
||||
<glyph unicode="📁" horiz-adv-x="1232" d="M1081 666q33 0 41 -12.5t6 -37.5l-43 -462q-2 -25 -12 -38.5t-43 -13.5h-825q-53 0 -58 52l-43 462q-2 25 6.5 37.5t41.5 12.5h929zM1047 778l10 -41h-867l15 135q4 20 20.5 35t36.5 15h168q53 0 88 -35l31 -31q33 -37 88 -37h348q20 0 39 -12.5t23 -28.5z" />
|
||||
<glyph unicode="📄" horiz-adv-x="921" d="M319 469v92h287v-92h-287zM717 973q43 0 72.5 -30t29.5 -73v-716q0 -41 -29.5 -72t-72.5 -31h-512q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h512zM717 154v716h-512v-716h512zM604 760v-90h-287v90h287zM604 358v-90h-287v90h287z" />
|
||||
<glyph unicode="📅" horiz-adv-x="1126" d="M922 870q43 0 72.5 -29.5t29.5 -72.5v-614q0 -41 -29.5 -72t-72.5 -31h-717q-41 0 -72 31t-31 72v614q0 43 31 72.5t72 29.5h47v-102h164v102h297v-102h164v102h45zM922 154v409h-717v-409h717zM369 973v-174h-72v174h72zM829 973v-174h-71v174h71z" />
|
||||
<glyph unicode="📈" horiz-adv-x="1230" d="M137 444q-43 10 -33 58q10 43 56 33l100 -25l-53 -82zM1049 432q14 12 33.5 11t31.5 -15q33 -33 -2 -66l-258 -231q-12 -12 -31 -12q-14 0 -28 10l-293 225l-56 15l52 82l37 -9q12 -4 16 -8l270 -209zM547 657l-359 -563q-12 -23 -38 -22q-12 0 -25 8q-16 10 -20.5 29.5 t6.5 33.5l383 602q8 16 28 21q18 6 37 -6l252 -160l231 334q10 16 29 19t35 -9q39 -25 12 -63l-258 -371q-25 -37 -63 -12z" />
|
||||
<glyph unicode="📊" horiz-adv-x="1024" d="M870 973q23 0 37.5 -15.5t14.5 -35.5v-871h-205v871q0 51 41 51h112zM563 666q23 0 37 -15.5t14 -36.5v-563h-204v563q0 51 41 52h112zM256 358q23 0 37 -15t14 -36v-256h-205v256q0 51 41 51h113z" />
|
||||
<glyph unicode="📋" horiz-adv-x="921" d="M748 922q29 0 50 -21.5t21 -50.5v-778q0 -31 -21.5 -51.5t-49.5 -20.5h-574q-29 0 -50.5 20.5t-21.5 51.5v778q0 29 21.5 50.5t50.5 21.5l62 -154h450zM645 819h-369l-45 103h111l37 102h164l37 -102h112z" />
|
||||
<glyph unicode="📎" horiz-adv-x="1165" d="M352 10q-104 0 -174 74q-74 72 -76 170t86 195l508 507q82 82 178 56q45 -12 81 -48t49 -81q27 -98 -56 -181l-485 -485q-41 -41 -90 -47q-49 -4 -82 29q-31 25 -28 76t48 94l340 342q25 27 51.5 0t-0.5 -52l-340 -340q-45 -45 -20 -71q12 -8 25 -6q25 4 47 26l485 486 q51 51 35 110q-16 61 -78 78q-55 14 -111 -37l-505 -506q-68 -78 -66 -146.5t53 -119.5q51 -49 120 -51t145 63l507 506q25 25 52 0q27 -23 0 -49l-508 -508q-85 -84 -191 -84z" />
|
||||
<glyph unicode="📑" horiz-adv-x="768" d="M614 1024q20 0 36 -15.5t16 -35.5v-871l-154 185v635q0 20 -15.5 35.5t-35.5 15.5h-103q0 51 41 51h215zM358 870q20 0 36 -15t16 -36v-819l-154 184l-154 -184v819q0 51 41 51h215z" />
|
||||
<glyph unicode="📕" horiz-adv-x="921" d="M801 762q18 -8 18 -29v-575q0 -14 -12 -25.5t-29 -11.5q-47 0 -47 37v534q0 12 -12 19l-414 221q-33 10 -69 -10q-45 -20 -58 -45l418 -234q18 -8 18 -29v-563q0 -23 -18 -28q-6 -4 -16 -5q-14 0 -21 5q-8 6 -206.5 129.5t-217.5 134.5q-27 18 -26 35l-7 536q0 29 15 53 q29 47 104.5 79t118.5 9z" />
|
||||
<glyph unicode="📖" horiz-adv-x="1126" d="M451 397v-69l-205 82v69zM451 610v-69l-205 82v69zM1001 965q23 -12 23 -43v-656q0 -35 -33 -47l-407 -164q-8 -2 -10.5 -2t-5.5 -1t-5 -1t-5 1t-5 1l-10 2l-408 164q-33 12 -33 47v656q0 31 23 43q23 16 47 6l391 -158l391 158q24 10 47 -6zM512 158v573l-328 131v-573z M942 289v573l-328 -131v-573zM881 479v-69l-205 -82v69zM881 692v-69l-205 -82v69z" />
|
||||
<glyph unicode="📞" horiz-adv-x="1024" d="M575 451q166 166 121 211l-8 8q-31 31 -42 49t-4 55t50 90q20 25 38 40.5t36 16.5t30.5 1t30 -13.5t24.5 -18.5t26.5 -25.5t21.5 -22.5q49 -49 -6 -198.5t-209 -301.5q-154 -154 -302.5 -210t-197.5 -7q-2 2 -23.5 22.5t-25.5 25.5t-18.5 24.5t-13.5 32t2 31t15.5 35.5 t39.5 38q43 35 71.5 48t55 2t36 -18.5t39.5 -37.5q45 -45 213 123z" />
|
||||
<glyph unicode="📣" horiz-adv-x="1087" d="M913 666q59 -141 68.5 -264t-39.5 -144q-29 -12 -62.5 3t-66.5 41t-101.5 42t-152.5 8q-29 -4 -43 -19.5t-6 -37.5q23 -57 47 -111q4 -10 24.5 -22t24.5 -21q14 -35 -22 -47q-51 -23 -105 -41q-31 -10 -55 43q-33 78 -59 135q-6 12 -35 17.5t-47 32.5q-31 -10 -39 -15 q-35 -12 -76 12.5t-55 61.5q-16 33 -5 81t44 62q129 53 218 110.5t127 106t60.5 94.5t25.5 79.5t15 60.5t37 37q49 20 133 -72t145 -233zM885 358q8 4 10 39t-11.5 100.5t-41.5 131.5q-29 68 -69 126t-68.5 85.5t-37 23.5t-10.5 -43t10.5 -107.5t41 -136t69.5 -122t70 -78 t37 -19.5z" />
|
||||
<glyph unicode="📤" horiz-adv-x="1230" d="M614 948l267 -250h-168v-262h-195v262h-170zM1094 356q18 -10 27 -32.5t3 -40.5l-28 -158q-4 -20 -22.5 -33.5t-41.5 -13.5h-835q-23 0 -41.5 13t-22.5 34l-29 158q-10 49 33 73l162 111h100l-174 -133h183q8 0 12 -8l41 -113h307l41 113q8 8 12 8h183l-175 133h101z" />
|
||||
<glyph unicode="📥" horiz-adv-x="1230" d="M1094 356q18 -10 27 -32.5t3 -40.5l-28 -158q-4 -20 -22.5 -33.5t-41.5 -13.5h-835q-23 0 -41.5 13t-22.5 34l-29 158q-10 49 33 73l162 111h100l-174 -133h183q8 0 12 -8l41 -113h307l41 113q8 8 12 8h183l-175 133h101zM881 686l-267 -250l-266 250h170v262h195v-262 h168z" />
|
||||
<glyph unicode="📦" horiz-adv-x="1126" d="M993 922q12 0 21.5 -9.5t9.5 -21.5v-123h-922v123q0 12 9.5 21.5t21.5 9.5h860zM154 174v543h819v-543q0 -31 -21.5 -51.5t-50.5 -20.5h-676q-29 0 -50 20.5t-21 51.5zM410 614v-102h307v102h-307z" />
|
||||
<glyph unicode="📰" horiz-adv-x="1024" d="M819 973q43 0 73 -30t30 -73v-716q0 -41 -30 -72t-73 -31h-614q-41 0 -72 31t-31 72v716q0 43 31 73t72 30h614zM819 154v716h-614v-716h614zM563 410v-52h-256v52h256zM717 614v-51h-205v51h205zM512 666v102h205v-102h-205zM461 768v-205h-154v205h154zM410 512v-51 h-103v51h103zM461 461v51h256v-51h-256zM717 307v-51h-410v51h410zM614 358v52h103v-52h-103z" />
|
||||
<glyph unicode="📱" horiz-adv-x="798" d="M594 1014q43 0 72.5 -30t29.5 -73v-798q0 -41 -29.5 -72t-72.5 -31h-389q-41 0 -72 31t-31 72v798q0 43 31 73t72 30h389zM399 51q31 0 51.5 15.5t20.5 35.5q0 23 -20.5 37.5t-51.5 14.5q-29 0 -50 -15.5t-21 -36t21 -36t50 -15.5zM614 205v676h-430v-676h430z" />
|
||||
<glyph unicode="📶" horiz-adv-x="1208" d="M604 307q43 0 73 -30.5t30 -71.5q0 -43 -30 -73t-73 -30q-41 0 -70.5 30t-29.5 73q0 41 29.5 71.5t70.5 30.5zM389 422q90 90 215 90t215 -90l-71 -74q-59 59 -143.5 59.5t-143.5 -59.5zM246 565q150 152 359.5 152t357.5 -152l-72 -71q-119 121 -286 120.5t-288 -120.5z M102 711q209 211 503 211t501 -211l-72 -72q-178 180 -430 180t-430 -180z" />
|
||||
<glyph unicode="📷" d="M614 614q66 0 110 -45t44 -108.5t-45 -108.5t-108.5 -45t-108.5 45t-45 108.5t45 108.5t108 45zM1024 768q43 0 72.5 -29.5t29.5 -72.5v-461q0 -41 -29.5 -72t-72.5 -31h-819q-41 0 -72 31t-31 72v461q0 43 31 72.5t72 29.5h123q29 0 41 31l30 94q10 29 41 29h348 q31 0 41 -29l31 -94q12 -31 41 -31h123zM614.5 205q106.5 0 181 74.5t74.5 181t-74.5 181.5t-181 75t-181.5 -75t-75 -181.5t75 -181t181.5 -74.5zM989.5 594q14.5 0 24.5 11t10 25.5t-10 25t-25 10.5q-37 0 -37 -35q0 -16 11.5 -26.5t26 -10.5z" />
|
||||
<glyph unicode="📸" horiz-adv-x="921" d="M805 367q14 20 14 -2v-103q0 -76 -106.5 -138.5t-251.5 -62.5q-143 0 -251 62.5t-108 138.5v103q0 8 4.5 10t10.5 -8q33 -53 128 -88t216 -35t216 35t128 88zM807 627q8 16 12 0v-119q0 -70 -104.5 -117t-253.5 -47q-147 0 -253 47t-106 117v119q0 20 15 0 q31 -47 127 -77t217 -30t217 30t129 77zM460.5 963q147.5 0 253 -40t105.5 -96v-65q0 -59 -105.5 -101.5t-253 -42.5t-253 42t-105.5 102v65q0 55 105.5 95.5t253 40.5z" />
|
||||
<glyph unicode="📽" horiz-adv-x="1128" d="M1008 465q27 -45 14 -98l-35 -189q-2 -20 -19.5 -35.5t-39.5 -15.5h-729q-20 0 -39 15.5t-21 35.5l-35 189q-8 53 15 98l162 383q23 47 73 47h418q51 0 76 -47zM938 330q2 23 -10.5 39t-34.5 16h-660q-23 0 -35 -16.5t-10 -38.5l15 -76q2 -23 19.5 -38t39.5 -15h604 q23 0 40.5 15t19.5 38z" />
|
||||
<glyph unicode="📾" horiz-adv-x="1142" d="M639 952q178 0 293 -50t106 -107q-6 -39 -49 -314.5t-45 -287.5q-2 -18 -38 -45t-109.5 -51.5t-157.5 -24.5t-156.5 24.5t-108.5 51t-38 45.5q0 2 -4 30q84 -6 167 36t142 120q29 0 49.5 20.5t20.5 51.5q0 29 -20.5 50t-51.5 21q-29 0 -50.5 -21.5t-21.5 -49.5 q0 -20 11 -37q-49 -59 -118 -91t-134 -28q-104 10 -160.5 58t-61.5 112q-8 125 160 188q-18 96 -22 142q-8 57 106.5 107t290.5 50zM176 471q4 -33 38 -60.5t93 -39.5l-33 209q-102 -46 -98 -109zM639 688q84 0 161 18.5t115.5 40t38.5 36t-38.5 36t-114.5 40t-162 18.5 q-84 0 -160 -18.5t-114.5 -40t-38.5 -36t38.5 -36t114.5 -40t160 -18.5z" />
|
||||
<glyph unicode="📿" horiz-adv-x="757" d="M512 518q66 -37 104.5 -100.5t38.5 -141.5q0 -115 -80.5 -195.5t-195.5 -80.5t-196 81t-81 195q0 78 39 141.5t105 100.5v455q0 51 41 51h174q20 0 35.5 -15.5t15.5 -35.5v-455zM379 102q72 0 123 51.5t51 122.5q0 57 -33 102.5t-86 61.5v379h-102v-377q-55 -16 -91 -62 t-36 -104q0 -72 51 -123t123 -51z" />
|
||||
<glyph unicode="🔀" d="M874 682q-55 0 -107 -33t-82 -67.5t-85 -106.5q-49 -63 -76.5 -96t-80 -79t-110 -67.5t-124.5 -21.5h-107v143h107q55 0 108.5 33t83 67.5t84.5 106.5q63 84 103.5 129t119 90t166.5 45h37v123l215 -184l-215 -184v102h-37zM379 592q-76 80 -170 80h-107v143h107 q143 0 260 -110q-14 -16 -37.5 -46t-28.5 -34q-8 -13 -24 -33zM911 344v102l215 -184l-215 -184v123h-37q-143 0 -266 118q47 59 74 95q0 2 6 9t8 11q86 -90 178 -90h37z" />
|
||||
<glyph unicode="🔁" horiz-adv-x="1126" d="M922 707q43 0 72.5 -30t29.5 -73v-297q0 -41 -29.5 -71.5t-72.5 -30.5h-717q-41 0 -72 30.5t-31 71.5v297q0 43 31 73t72 30h256v112l205 -184l-205 -184v112h-215v-215h635v215h-154v144h195z" />
|
||||
<glyph unicode="🔄" horiz-adv-x="1048" d="M295 297l119 119v-299l-283 16l90 88q-119 125 -117 297t123 295q102 102 246 119l4 -105q-102 -16 -176 -90q-90 -90 -92 -218t86 -222zM635 909l283 -16l-91 -88q119 -125 117 -297t-123 -295q-98 -100 -246 -121l-2 107q100 16 175 90q90 90 92 218t-86 222l-117 -119 z" />
|
||||
<glyph unicode="🔅" horiz-adv-x="921" d="M461 676q70 0 117 -48t47 -116q0 -70 -47.5 -117t-116.5 -47q-68 0 -116 47t-48 117q0 68 48 116t116 48zM461 406q45 0 74.5 30.5t29.5 75.5q0 43 -29.5 73.5t-74.5 30.5q-43 0 -74 -30.5t-31 -73.5q0 -45 31 -75.5t74 -30.5zM153.5 553q20.5 0 36 -12.5t15.5 -28.5 q0 -41 -51.5 -41t-51.5 41q0 16 15.5 28.5t36 12.5zM712.5 762q28.5 -29 -7.5 -66q-14 -14 -34 -16t-30 10q-12 12 -10 31.5t16 34.5q37 35 65.5 6zM768 553q20 0 35.5 -12.5t15.5 -28.5q0 -41 -51 -41q-49 0 -49 41q0 16 14.5 28.5t34.5 12.5zM460.5 256q16.5 0 29 -15.5 t12.5 -36t-12.5 -35.5t-29 -15t-28.5 15t-12 35.5t12 36t28.5 15.5zM217 319.5q37 36.5 65.5 8t-8.5 -65.5q-14 -14 -33.5 -16t-29.5 8q-31 29 6 65.5zM207 759.5q29 28.5 65 -7.5q14 -14 16.5 -34t-7.5 -30q-31 -29 -66 6q-37 37 -8 65.5zM649 264q-35 37 -6 65.5t66 -7.5 q14 -14 16 -34t-10 -30q-31 -29 -66 6zM460.5 768q-16.5 0 -28.5 15.5t-12 36t12 35.5t28.5 15t29 -15t12.5 -35.5t-12.5 -36t-29 -15.5z" />
|
||||
<glyph unicode="🔆" d="M1075 553q20 0 35.5 -12.5t15.5 -28.5q0 -41 -51 -41h-49q-51 0 -51 41q0 16 15.5 28.5t35.5 12.5h49zM614.5 793q116.5 0 199.5 -82t83 -199q0 -119 -83 -201t-199.5 -82t-198.5 82t-82 201q0 117 82 199t198.5 82zM614 307q84 0 144.5 59.5t60.5 145.5 q0 84 -60.5 144.5t-144.5 60.5t-144 -60.5t-60 -144.5q0 -86 60 -145.5t144 -59.5zM256 512q0 -41 -51 -41h-51q-51 0 -52 41q0 16 15.5 28.5t36.5 12.5h51q20 0 35.5 -12.5t15.5 -28.5zM614.5 870q-16.5 0 -29 15.5t-12.5 36.5v51q0 20 12.5 35.5t29 15.5t28.5 -15.5 t12 -35.5v-51q0 -20 -12 -36t-28.5 -16zM614.5 154q16.5 0 28.5 -15.5t12 -36.5v-51q0 -20 -12 -35.5t-28.5 -15.5t-29 15.5t-12.5 35.5v51q0 20 12.5 36t29 16zM991 829l-35 -34q-35 -35 -65 -9q-29 29 8 66q4 6 35 37q37 35 65.5 6t-8.5 -66zM274 227q14 16 34 18.5 t30 -9.5q12 -12 10 -32t-16 -34l-37 -37q-14 -14 -33.5 -16t-30.5 10q-31 29 7 66q5 3 36 34zM295 889l37 -37q37 -37 6 -66q-10 -10 -29.5 -8t-34.5 17q-31 31 -36 34q-14 14 -16.5 34t9.5 32q10 12 30 10t34 -16zM899 170q-37 37 -8 65.5t65 -8.5l35 -34q37 -37 8.5 -66 t-65.5 6q-31 31 -35 37z" />
|
||||
<glyph unicode="🔇" horiz-adv-x="1110" d="M991 950q16 -16 16.5 -36.5t-16.5 -36.5l-801 -801q-18 -14 -34 -15q-18 0 -37 15q-16 14 -16.5 36.5t16.5 37.5l801 800q34 33 71 0zM770 555l51 51q76 -94 103.5 -176t-6.5 -119q-25 -25 -77.5 -58.5t-134 -72.5t-164.5 -46t-169 24l284 282q45 -33 90.5 -55.5 t69 -25.5t33.5 1q6 10 2 35t-26.5 70t-55.5 90zM487 618l-276 -276q-41 135 28.5 289.5t135.5 220.5q35 33 107.5 11.5t162.5 -87.5l-53 -51q-59 39 -107.5 54.5t-58.5 4.5q-4 -8 -2 -28.5t19 -59.5t44 -78z" />
|
||||
<glyph unicode="🔊" horiz-adv-x="1138" d="M283 756q43 43 152.5 -5.5t222 -161t160.5 -222t5 -152.5q-29 -29 -94 -69t-159.5 -79.5t-199 -29.5t-180 86t-86 180.5t29.5 198.5t80 159.5t69 94.5zM758 264q8 10 -3.5 50t-50 103.5t-98.5 121.5q-57 59 -120.5 98t-103.5 50t-50 3q-8 -10 3 -50t50 -103.5t96 -122.5 q59 -57 123 -96t104 -50.5t50 -3.5zM764 668q-18 0 -35 16q-16 14 -16 35.5t16 36.5l96 98q37 33 74 0q33 -37 0 -74l-98 -96q-17 -16 -37 -16zM580 795q-18 10 -23.5 30.5t4.5 38.5l55 99q27 45 70 20q18 -10 23.5 -30.5t-4.5 -39.5l-56 -98q-14 -27 -43 -27q-14 1 -26 7z M1028 641q10 -18 4 -38.5t-24 -31.5l-99 -55q-16 -8 -24 -8q-29 0 -45 27q-10 18 -4 38.5t24 30.5l98 55q18 10 39 5t31 -23z" />
|
||||
<glyph unicode="🔋" d="M891 512q0 -100 37 -160.5t80 -60.5h67q-31 -47 -65.5 -66.5t-120.5 -19.5h-512q-133 0 -204 96t-71 211q0 113 71 210t204 97h512q86 0 120.5 -19.5t65.5 -66.5h-67q-43 0 -80 -61.5t-37 -159.5zM752 420q10 12 -9 26q-139 137 -182 168q-16 10 -26.5 13.5t-18.5 -5 t-10 -12.5t-8 -18l-23 -57l-151 67q-27 12 -35 0q-8 -14 8 -29q139 -135 184 -165q35 -16 43 -11.5t19 31.5l24 59l150 -69q27 -12 35 2zM1069 616q23 0 40 -27.5t17 -72.5t-17 -73.5t-40 -28.5h-39q-23 0 -39 28.5t-16 73.5t16.5 72.5t38.5 27.5h39z" />
|
||||
<glyph unicode="🔍" horiz-adv-x="1013" d="M893 233q31 -35 6 -63l-47 -47q-37 -33 -70 0l-194 194q-76 -43 -160 -43q-131 0 -228.5 97.5t-97.5 228.5t92.5 224.5t223.5 93.5t229 -97.5t98 -228.5q0 -90 -47 -166zM199 600q0 -90 69.5 -159.5t159.5 -69.5t154.5 64.5t64.5 156.5q0 90 -69.5 158.5t-159.5 68.5 t-154.5 -64.5t-64.5 -154.5z" />
|
||||
<glyph unicode="🔑" horiz-adv-x="1003" d="M895 780q20 -119 -29 -220t-153 -120q-68 -12 -133 -2l-121 -198l-72 -13l-106 -170q-14 -29 -48 -32l-77 -15q-12 -4 -22.5 4.5t-12.5 22.5l-17 100q-8 31 13 58l264 395q-25 51 -39 123q-18 109 54.5 191.5t189.5 103.5q109 20 200 -46.5t109 -181.5zM766 702 q31 45 21.5 99.5t-52.5 85.5q-43 33 -94 22.5t-82 -55.5q-8 -12 -12 -23.5t-1 -20.5t5 -16.5t13 -17.5t18.5 -15t23 -16.5t23.5 -17.5q6 -4 22.5 -16.5t23.5 -16.5t19.5 -12t19.5 -8t17 1t18.5 8t16.5 19z" />
|
||||
<glyph unicode="🔒" horiz-adv-x="921" d="M758 641q20 0 40.5 -19.5t20.5 -41.5v-400q0 -49 -49 -67l-61 -19q-43 -16 -99 -16h-297q-57 0 -100 16l-61 19q-49 18 -50 67v400q0 23 15.5 42t36.5 19h102v72q0 113 52 174t152.5 61t153 -61.5t52.5 -173.5v-72h92zM358 733v-92h205v92q0 53 -27.5 83t-74.5 30 t-75 -30t-28 -83z" />
|
||||
<glyph unicode="🔓" horiz-adv-x="921" d="M758 614q20 0 40.5 -20.5t20.5 -40.5v-399q0 -20 -14 -40t-35 -26l-61 -20q-53 -16 -99 -17h-297q-47 0 -100 17l-61 20q-20 6 -35 25.5t-15 40.5v399q0 23 15.5 42t36.5 19h409v144q0 113 -102.5 112.5t-102.5 -112.5v-41h-102v20q0 113 52 174.5t153 61.5 q205 0 205 -236v-123h92z" />
|
||||
<glyph unicode="🔔" horiz-adv-x="1024" d="M750 590q16 -35 40.5 -53.5t46 -22.5t45 -23.5t36.5 -56.5q23 -63 -76 -164.5t-258 -160.5q-168 -59 -304.5 -46t-158.5 76q-20 55 12.5 113.5t18.5 114.5q-57 197 -48 307t115 196q27 23 30 52.5t30 40.5q25 8 47 -12.5t57 -18.5q135 2 203 -67.5t164 -274.5zM559 176 q90 33 163 87t102.5 93t25.5 52q-8 23 -50 34t-127 0.5t-192 -49.5q-104 -39 -177 -89t-96.5 -86t-17.5 -54q4 -12 51.5 -22.5t137.5 -4.5t180 39zM496 354q8 2 21 7.5t18 7.5l2 -2q14 -41 -18 -85t-91 -65q-98 -37 -156 14q81 70 224 123z" />
|
||||
<glyph unicode="🔖" horiz-adv-x="573" d="M420 973q23 0 37 -15.5t14 -35.5v-871l-184 185l-185 -185v871q0 51 41 51h277z" />
|
||||
<glyph unicode="🔗" horiz-adv-x="1024" d="M403 272q14 14 35 14.5t37 -14.5q33 -35 0 -71l-43 -41q-57 -57 -135 -58q-80 0 -137.5 57.5t-57.5 135.5q0 80 58 137l151 152q72 70 148 79t131 -45q16 -16 16 -36.5t-16 -36.5q-37 -33 -72 0q-51 49 -135 -35l-152 -150q-27 -27 -26.5 -65.5t26.5 -63.5 q27 -27 65 -26.5t64 26.5zM864 860q57 -57 58 -135q0 -80 -58 -137l-162 -162q-76 -74 -153 -74q-63 0 -115 51q-14 14 -14 35t14 37q14 14 36 14t36 -14q51 -49 125 25l162 159q29 29 28 66q0 39 -28 63q-25 27 -58 32t-61 -21l-51 -51q-16 -14 -37 -14.5t-35 14.5 q-35 35 0 71l51 51q55 55 130 52t132 -62z" />
|
||||
<glyph unicode="🔙" horiz-adv-x="1075" d="M870 707q41 0 72 -30t31 -73v-297q0 -41 -31 -71.5t-72 -30.5h-706v143h665v215h-512v-112l-215 184l215 184v-112h553z" />
|
||||
<glyph unicode="🔦" horiz-adv-x="1128" d="M929.5 876.5q63.5 -63.5 87 -133t-4.5 -94.5l-138 -137q-16 -16 -63 -26.5t-98 -4.5l-418 -418q-18 -18 -58 -5.5t-77 51.5q-37 37 -50.5 75.5t5.5 57.5l417 417q-6 51 4.5 98.5t26.5 63.5l139 140q25 29 94.5 4t133 -88.5zM471 459q33 -33 82 14q47 47 14 84 q-14 14 -38.5 10t-45 -24.5t-23.5 -44t11 -39.5zM815 764q31 -31 70 -51.5t63.5 -25.5t28.5 -1q2 4 -4 27.5t-26.5 61.5t-51.5 68.5t-67.5 51t-60.5 27t-28 2.5t1 -29t25.5 -63.5t49.5 -67.5z" />
|
||||
<glyph unicode="🔾" horiz-adv-x="1208" d="M1090 903q16 23 16 -4v-786h-987q-12 0 -15.5 7t5.5 17l235 295q20 23 41 2l76 -67q10 -8 21.5 -7.5t17.5 11.5l162 243q16 27 38 4l115 -106q20 -20 39 4z" />
|
||||
<glyph unicode="🔿" horiz-adv-x="1150" d="M1020 891q14 4 22.5 -3t4.5 -20q-2 -6 -74 -317t-76 -324q-2 -14 -14.5 -19t-24.5 1l-254 137l-31 16l23 27q397 430 403 436q4 4 -1 9.5t-9 1.5l-563 -412l-115 45l-194 78q-12 4 -12.5 12t12.5 12q8 4 451.5 161t451.5 159zM424 145v209l164 -84q-133 -119 -146 -131 q-18 -14 -18 6z" />
|
||||
<glyph unicode="🕅" horiz-adv-x="1191" d="M590 1004q203 2 348 -139.5t149 -344.5q2 -205 -139 -350t-346 -150q-203 -2 -349.5 140.5t-148.5 345.5q-4 205 138.5 350.5t347.5 147.5zM602 125q160 2 272.5 116.5t110.5 276.5t-117.5 273.5t-275.5 109.5q-162 -2 -273.5 -116.5t-109.5 -276.5t116.5 -273.5 t276.5 -109.5zM475 430q41 0 60 41l57 -31q-20 -37 -51 -53q-33 -20 -72 -20q-63 0 -102 39q-39 37 -39 106.5t39 108t98 38.5q88 0 127 -67l-64 -33q-10 20 -24.5 28.5t-28.5 8.5q-61 0 -61 -84q0 -39 14 -59q18 -23 47 -23zM748 430q43 0 57 41l59 -31q-18 -33 -51 -53 t-72 -20q-66 0 -102 39q-39 37 -39 106q0 66 39 109q39 39 100 38q86 0 123 -67l-61 -33q-10 20 -24.5 28.5t-28.5 8.5q-63 0 -64 -84q0 -37 16.5 -59.5t47.5 -22.5z" />
|
||||
<glyph unicode="🕆" horiz-adv-x="1187" d="M594 692q-68 0 -68 69.5t68 69.5q70 0 70 -69.5t-70 -69.5zM694 666q14 0 23 -9q10 -10 10 -22v-201h-57v-239h-152v239h-57v201q0 12 10 22q8 8 23 9h200zM594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5 q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5t-283.5 116.5t-283.5 -116.5t-117.5 -284.5q0 -166 117.5 -283.5t283.5 -117.5z" />
|
||||
<glyph unicode="🕇" horiz-adv-x="1187" d="M594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM215 645q-23 -63 -22 -133q0 -166 117.5 -283.5t283.5 -117.5q113 0 207 57t145 151l-182 82q-8 -47 -47 -75q-39 -31 -88 -35 v-76h-58v76q-80 0 -149 59l67 68q51 -45 111 -45q25 0 43.5 12t18.5 37q0 18 -15 31l-47 20l-57 27l-78 32zM733 520l248 -110q14 45 14 102q0 168 -117.5 284.5t-283.5 116.5q-104 0 -192.5 -49t-143.5 -133l186 -84q12 37 48 64q33 23 79 24v76h58v-76q70 -4 123 -45 l-64 -65q-45 29 -86 28q-25 0 -39 -8q-18 -10 -18 -31q0 -8 4 -12l61 -29l43 -18z" />
|
||||
<glyph unicode="🕈" horiz-adv-x="1187" d="M594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM215 643q-23 -63 -22 -131q0 -166 117.5 -283.5t283.5 -117.5q113 0 206 56t146 152l-252 111h-178q10 -37 27 -57 q39 -41 106 -41q47 0 94 20l19 -92q-57 -31 -127 -31q-131 0 -201 95q-35 45 -47 106h-53v59h45v15q0 4 1 12t1 12h-47v58h10zM715 528l268 -118q12 49 12 102q0 168 -117.5 284.5t-283.5 116.5q-104 0 -193.5 -49t-144.5 -133l162 -72q8 14 28 39q74 84 189 84 q72 0 125 -24l-25 -94q-41 20 -90 20q-66 0 -102 -45q-10 -10 -17 -29l58 -24h139v-58h-8z" />
|
||||
<glyph unicode="🕉" horiz-adv-x="1187" d="M594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM211 631q-18 -55 -18 -119q0 -166 117.5 -283.5t283.5 -117.5q109 0 200 53t144 143l-156 70v-70h-129v-110h-121v110h-126v76 h126v37l-12 24h-114v76h55zM653 383h115l-109 49l-6 -12v-37zM782 485l197 -88q16 59 16 115q0 168 -117.5 284.5t-283.5 116.5q-109 0 -199 -52t-143 -140l162 -72l-56 101h131l78 -170l47 -21l84 191h131l-124 -230h77v-35z" />
|
||||
<glyph unicode="🕊" horiz-adv-x="1187" d="M592 772q117 0 184 -76q68 -74 68 -190q0 -113 -70 -189q-76 -76 -184 -75q-82 0 -146 51q-59 49 -71 141h123q6 -88 108 -88q51 0 84 43q31 45 31 121q0 78 -29 119q-31 41 -84 41q-98 0 -110 -88h36l-98 -97l-96 97h37q14 92 74 141q59 49 143 49zM594 1004 q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5t-283.5 116.5t-283.5 -116.5t-117.5 -284.5q0 -166 117.5 -283.5t283.5 -117.5 z" />
|
||||
<glyph unicode="🕋" horiz-adv-x="1187" d="M416 545v84h356v-84h-356zM416 387v84h356v-84h-356zM594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5 t-283.5 116.5t-283.5 -116.5t-117.5 -284.5q0 -166 117.5 -283.5t283.5 -117.5z" />
|
||||
<glyph unicode="🕌" horiz-adv-x="1187" d="M594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM215 643q-23 -63 -22 -131q0 -166 117.5 -283.5t283.5 -117.5q113 0 206 56t146 152l-434 193q2 -55 28.5 -98t77.5 -43 q37 0 66 26l6 6l72 -86q-4 -2 -10.5 -7t-8.5 -9q-63 -43 -139 -43q-88 0 -162.5 59.5t-74.5 192.5q0 33 6 63zM532 608l451 -198q12 49 12 102q0 168 -117.5 284.5t-283.5 116.5q-104 0 -193.5 -49t-144.5 -133l152 -67q66 104 200 104q90 0 154 -55l-80 -82q-8 8 -14 12 q-23 16 -54 16q-53 0 -82 -51z" />
|
||||
<glyph unicode="🕍" horiz-adv-x="1187" d="M594 797q111 0 157 -83t46 -202q0 -117 -46 -200t-157 -83t-157 83t-46 200q0 119 46 202t157 83zM506 512q0 -18 4 -68l108 199q14 25 -6 43q-12 4 -18 4q-88 0 -88 -178zM594 336q88 0 88 176q0 41 -6 86l-121 -209q-23 -31 12 -47q2 -2 6 -2q2 0 2 -2q2 0 8.5 -1 t10.5 -1zM594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5t-283.5 116.5t-283.5 -116.5t-117.5 -284.5 q0 -166 117.5 -283.5t283.5 -117.5z" />
|
||||
<glyph unicode="🕎" horiz-adv-x="1187" d="M795 653q12 0 20 -8t8 -18v-363q0 -10 -8 -18t-20 -8h-267q-12 0 -20 8t-8 18v107h-107q-10 0 -18 8t-8 20v361q0 12 6 18q4 6 18 10h271q10 0 18 -8t8 -20v-107h107zM524 653h111v80h-215v-307h80v201q0 10 8 18q4 4 16 8zM768 291v309h-215v-309h215zM594 1004 q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5t-283.5 116.5t-283.5 -116.5t-117.5 -284.5q0 -166 117.5 -283.5t283.5 -117.5 z" />
|
||||
<glyph unicode="🕏" horiz-adv-x="1187" d="M915 504l11 -4v-140l-11 -4l-118 -51l-4 -2l-7 2l-258 107l-8 4l-127 -54l-127 56v125l119 49l-2 2v139l133 58l301 -125v-121zM776 342v88h-2v2l-225 92v-88l225 -94v2zM791 457l79 32l-73 31l-78 -33zM895 381v86l-88 -37v-86zM594 1004q205 0 348 -143.5t143 -348.5 q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM594 111q166 0 283.5 117.5t117.5 283.5q0 168 -117.5 284.5t-283.5 116.5t-283.5 -116.5t-117.5 -284.5q0 -166 117.5 -283.5t283.5 -117.5z" />
|
||||
<glyph unicode="🕔" horiz-adv-x="1146" d="M573.5 983q194.5 0 332.5 -138t138 -333t-138 -333t-332.5 -138t-333 138t-138.5 333t138.5 333t333 138zM573 143q154 0 261.5 108.5t107.5 260.5q0 154 -107.5 261.5t-261.5 107.5q-152 0 -260 -107.5t-108 -261.5q0 -152 108 -260.5t260 -108.5zM610 778v-250 l154 -153l-51 -51l-174 174v280h71z" />
|
||||
<glyph unicode="🕨" horiz-adv-x="860" d="M635 276q51 0 87 -35.5t36 -87t-36 -87t-87 -35.5q-82 0 -113 75h-168q-90 0 -134 55.5t-44 121.5v475q-74 35 -74 112q0 51 36 87t87 36t87 -35.5t36 -87.5q0 -78 -74 -112v-117q0 -80 80 -80h168q31 74 113 74q51 0 87 -36t36 -87t-36 -87t-87 -36q-82 0 -113 76h-168 q-43 0 -80 16v-198q0 -80 80 -80h168q31 73 113 73zM635 584q-29 0 -50.5 -20.5t-21.5 -51.5q0 -29 21.5 -49.5t50.5 -20.5t50.5 20.5t21.5 49.5q0 31 -21.5 51.5t-50.5 20.5zM154 870q0 -29 21.5 -49t50 -20t50 20.5t21.5 48.5q0 31 -21.5 51.5t-50 20.5t-50 -20.5 t-21.5 -51.5zM635 84q29 0 50.5 20.5t21.5 49.5q0 31 -21.5 51t-50.5 20t-50.5 -20.5t-21.5 -50.5q0 -29 21.5 -49.5t50.5 -20.5z" />
|
||||
<glyph unicode="🕩" horiz-adv-x="860" d="M758 819q0 -82 -76 -112q-6 -59 -28.5 -103.5t-62.5 -71t-69.5 -39t-77.5 -26.5q-43 -14 -64.5 -22.5t-48 -25t-38.5 -41t-17 -61.5q72 -31 72 -112q0 -51 -36 -87t-87 -36t-87 36t-36 87q0 80 74 114v388q-74 35 -74 112q0 51 36 87t87 36t87 -36t36 -87q0 -78 -74 -112 v-209q41 31 142 61q59 18 85.5 29.5t52.5 42.5t30 78q-72 33 -72 110q0 51 36 87t87 36t87 -36t36 -87zM156 819q0 -29 20.5 -49t49 -20t50 20.5t21.5 48.5q0 31 -21.5 51.5t-50 20.5t-49 -20.5t-20.5 -51.5zM225.5 135q28.5 0 50 20.5t21.5 49.5q0 31 -21.5 51t-50 20 t-49 -20t-20.5 -51q0 -29 20.5 -49.5t49 -20.5zM635 750q29 0 50.5 20.5t21.5 48.5q0 31 -21.5 51.5t-50.5 20.5t-49.5 -20.5t-20.5 -51.5q0 -29 20.5 -49t49.5 -20z" />
|
||||
<glyph unicode="🕪" horiz-adv-x="1167" d="M991 268q74 -35 74 -114q0 -51 -36 -87t-87 -36t-87 35.5t-36 87.5q0 80 74 114v117q0 80 -78 80h-102q-45 0 -80 12v-209q74 -35 74 -114q0 -51 -36 -87t-87.5 -36t-87 35.5t-35.5 87.5q0 80 74 114v209q-31 -12 -78 -12h-103q-35 0 -54 -19.5t-22.5 -34t-3.5 -26.5 v-117q74 -35 74 -114q0 -51 -36 -87t-87 -36t-87 35.5t-36 87.5q0 80 74 114v117q0 66 44 121t134 55h103q78 0 78 53v144q-74 35 -74 112q0 51 35.5 87t87 36t87.5 -35.5t36 -87.5q0 -78 -74 -112v-144q0 -53 80 -53h102q88 0 132 -55t44 -121v-117zM297 154q0 31 -21.5 51 t-50 20t-49 -20.5t-20.5 -50.5q0 -29 20.5 -49.5t49 -20.5t50 20.5t21.5 49.5zM514 870q0 -29 20.5 -49t49 -20t50 20.5t21.5 48.5q0 31 -21.5 51.5t-50 20.5t-49 -20.5t-20.5 -51.5zM655 154q0 31 -21.5 51t-50 20t-49 -20.5t-20.5 -50.5q0 -29 20.5 -49.5t49 -20.5 t50 20.5t21.5 49.5zM942 84q29 0 50.5 20.5t21.5 49.5q0 31 -21.5 51t-50.5 20t-49.5 -20.5t-20.5 -50.5q0 -29 20.5 -49.5t49.5 -20.5z" />
|
||||
<glyph unicode="🕫" horiz-adv-x="450" d="M274 319q74 -35 74 -114q0 -51 -36 -87t-87 -36t-87 36t-36 87q0 80 74 114v388q-74 35 -74 112q0 51 36 87t87 36t87 -36t36 -87q0 -78 -74 -112v-388zM156 819q0 -29 20.5 -49t49 -20t50 20.5t21.5 48.5q0 31 -21.5 51.5t-50 20.5t-49 -20.5t-20.5 -51.5zM225.5 135 q28.5 0 50 20.5t21.5 49.5q0 31 -21.5 51t-50 20t-49 -20t-20.5 -51q0 -29 20.5 -49.5t49 -20.5z" />
|
||||
<glyph unicode="🕬" horiz-adv-x="860" d="M348 819q0 -78 -74 -112v-388q74 -35 74 -114q0 -51 -36 -87t-87 -36t-87 36t-36 87q0 80 74 114v388q-74 35 -74 112q0 51 36 87t87 36t87 -36t36 -87zM297 205q0 31 -21.5 51t-50 20t-49 -20t-20.5 -51q0 -29 20.5 -49.5t49 -20.5t50 20.5t21.5 49.5zM225.5 750 q28.5 0 50 20.5t21.5 48.5q0 31 -21.5 51.5t-50 20.5t-49 -20.5t-20.5 -51.5q0 -29 20.5 -49t49 -20zM684 319q74 -35 74 -114q0 -51 -36 -87t-87 -36t-87 36t-36 87q0 80 74 114v388q-74 35 -74 112q0 51 36 87t87 36t87 -36t36 -87q0 -78 -74 -112v-388zM565 819 q0 -29 20.5 -49t49.5 -20t50.5 20.5t21.5 48.5q0 31 -21.5 51.5t-50.5 20.5t-49.5 -20.5t-20.5 -51.5zM635 135q29 0 50.5 20.5t21.5 49.5q0 31 -21.5 51t-50.5 20t-49.5 -20t-20.5 -51q0 -29 20.5 -49.5t49.5 -20.5z" />
|
||||
<glyph unicode="🗹" horiz-adv-x="948" d="M662 399q49 -92 -5 -184q-43 -72 -117.5 -90.5t-154.5 27.5l-66 36l-110 -188l-57 33l210 366q-70 -10 -137 29l-123 72l191 331l125 -71q68 -41 94 -103l211 367l59 -35l-110 -190l65 -37q78 -45 100.5 -122t-20.5 -146q-49 -91 -155 -95zM707 709l-66 39l-127 -222 q0 -2 -2 -6l63 -37q53 -31 102.5 -17.5t78 63t15.5 99.5t-64 81zM256 481q53 -31 102.5 -17.5t78 62.5t15 100.5t-64.5 80.5l-65 38l-132 -227zM416 205q53 -31 102 -17.5t78 62.5t14.5 99t-65.5 81l-64 37l-4 -4l-127 -221z" />
|
||||
<glyph unicode="🗺" horiz-adv-x="1126" d="M563 973q283 0 372 -90t89 -371q0 -283 -89 -372t-372 -89q-281 0 -371 89t-90 372q0 281 90 371t371 90z" />
|
||||
<glyph unicode="🚀" horiz-adv-x="1087" d="M659 395q6 -51 8.5 -82.5t-8 -60.5t-13.5 -41t-35.5 -32.5t-46 -27t-72 -32t-86.5 -37.5q-33 -12 -46.5 4t-3.5 45l41 113l-133 135l-108 -41q-29 -12 -44.5 2t-2.5 47q12 31 31.5 81t27.5 66.5t22.5 46t25.5 37t29.5 20.5t42 13h53.5t73 -6q10 14 29.5 40t79 87.5 t121 106.5t148.5 76.5t169 19.5q8 0 14 -6q4 -4 6 -15q10 -84 -18.5 -172t-77.5 -154.5t-100.5 -120.5t-88.5 -83zM711 698q23 -23 55.5 -22.5t54.5 22.5q23 25 23 57.5t-23 57.5q-23 23 -55.5 23t-54.5 -23q-23 -25 -23 -57.5t23 -57.5z" />
|
||||
<glyph unicode="🚫" horiz-adv-x="1187" d="M594 1004q205 0 348 -143.5t143 -348.5q0 -203 -143 -347.5t-348 -144.5q-203 0 -347.5 144.5t-144.5 347.5q0 205 144.5 348.5t347.5 143.5zM858 778zM221 512q0 -135 84 -236l526 527q-102 84 -237 84q-156 0 -264.5 -109.5t-108.5 -265.5zM330 248zM594 139 q156 0 265.5 109.5t109.5 263.5q0 133 -84 238l-527 -527q101 -84 236 -84z" />
|
||||
<glyph unicode="🛆" d="M1024 819q43 0 72.5 -30.5t29.5 -71.5v-563q0 -43 -29.5 -73t-72.5 -30h-51v768h51zM102 717q0 41 31 71.5t72 30.5h51v-768h-51q-41 0 -72 30t-31 73v563zM788 926v-107h113v-768h-573v768h112v107q100 47 174 47t174 -47zM727 819v68q-53 25 -113 24q-55 0 -112 -24 v-68h225z" />
|
||||
<glyph unicode="🛇" d="M518 336q35 57 219.5 290.5t198.5 225.5q12 -6 -98.5 -284.5t-141.5 -334.5q-51 -88 -139 -36.5t-39 139.5zM614 725q-172 0 -290.5 -130t-118.5 -319q0 -31 2 -47q2 -23 -12.5 -38t-35 -17t-37 12.5t-18.5 34.5q0 8 -1 26.5t-1 28.5q0 231 148.5 391t363.5 160 q74 0 138 -18l-72 -88q-41 4 -66 4zM985 662q141 -158 141 -386q0 -39 -2 -57q-2 -20 -16 -33.5t-35 -13.5h-4q-23 4 -36 20.5t-11 36.5q2 14 2 47q0 154 -82 275q6 14 20.5 52.5t22.5 58.5z" />
|
||||
<glyph unicode="🛈" horiz-adv-x="1191" d="M596 406q-92 0 -159.5 27.5t-71.5 66.5q45 127 57 162q10 -29 60 -47.5t114 -18.5q66 0 115 18.5t59 47.5q12 -35 57 -162q-4 -39 -71.5 -66.5t-159.5 -27.5zM596 748q-98 0 -127 45l53 145q10 35 73.5 35t76.5 -35q12 -39 51 -145q-29 -45 -127 -45zM1047 354 q41 -16 42 -41.5t-38 -48.5l-361 -192q-39 -23 -93 -23t-93 23l-363 192q-39 20 -37 47t43 43l193 78l-23 -61q0 -49 82 -84t197 -35t196.5 35t84.5 84l-23 61z" />
|
||||
</font>
|
||||
</defs></svg>
|
After Width: | Height: | Size: 81 KiB |
BIN
data/adminer/fonts/entypo.ttf
Normal file
BIN
data/adminer/fonts/entypo.ttf
Normal file
Binary file not shown.
BIN
data/adminer/fonts/entypo.woff
Normal file
BIN
data/adminer/fonts/entypo.woff
Normal file
Binary file not shown.
BIN
data/adminer/logo.png
Normal file
BIN
data/adminer/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
50
data/download-wikipedia-dump.sh
Executable file
50
data/download-wikipedia-dump.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./download-wikipedia-dump.sh
|
||||
# Description: Download and extract Wikipedia database dumps.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
DUMP_DIRECTORY="dump"
|
||||
DOWNLOAD_DATE="20240420"
|
||||
# DOWNLOAD_DATE="latest"
|
||||
WIKIPEDIA_DUMP_URL="https://dumps.wikimedia.org/enwiki/${DOWNLOAD_DATE}/enwiki-${DOWNLOAD_DATE}-"
|
||||
|
||||
mkdir --parents "${DUMP_DIRECTORY}"
|
||||
|
||||
download_file() {
|
||||
local filename="${1}"
|
||||
local file_path_output="${DUMP_DIRECTORY}/${filename}"
|
||||
local file_url="${WIKIPEDIA_DUMP_URL}${filename}"
|
||||
|
||||
if [[ ! -f "${file_path_output%.gz}" ]]; then
|
||||
echo "Downloading \"${filename}\" from \"${file_url}\"..."
|
||||
wget --output-document="${file_path_output}" "${file_url}"
|
||||
else
|
||||
echo "File \"${filename%.gz}\" from \"${file_url}\" already exists."
|
||||
fi
|
||||
}
|
||||
|
||||
# download_file "page.sql.gz"
|
||||
# download_file "pagelinks.sql.gz"
|
||||
|
||||
extract_file() {
|
||||
local filename="${1}"
|
||||
local file_path_input="${DUMP_DIRECTORY}/${filename}"
|
||||
local file_path_output="${DUMP_DIRECTORY}/${filename%.gz}"
|
||||
|
||||
if [[ ! -f "${file_path_output}" ]]; then
|
||||
echo "Extracting \"${filename}\" to \"${file_path_output}\"..."
|
||||
gzip --decompress "${file_path_input}"
|
||||
|
||||
# `--keep` flag to keep the original file, not needed here.
|
||||
# gzip --decompress --keep "${file_path_input}"
|
||||
else
|
||||
echo "File \"${filename}\" already extracted."
|
||||
fi
|
||||
}
|
||||
|
||||
extract_file "page.sql.gz"
|
||||
extract_file "pagelinks.sql.gz"
|
8
data/execute-sql.sh
Executable file
8
data/execute-sql.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
/data/sql/0000-sql-init.sh
|
||||
|
||||
/data/sql-pages-inserts/0000-pages.sh
|
||||
/data/sql-internal-links-inserts/0000-internal-links.sh
|
||||
|
||||
/data/sql/0999-sql-end.sh
|
289
data/generate-sql-files.js
Normal file
289
data/generate-sql-files.js
Normal file
@ -0,0 +1,289 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import {
|
||||
extractRowsFromSQLValues,
|
||||
swapKeysAndValues,
|
||||
zeroPad,
|
||||
} from "./utils.js"
|
||||
|
||||
const SQL_DUMP_PATH = path.join(process.cwd(), "dump")
|
||||
const SQL_FILENAME_NUMBER_PAD = 4
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, number>} WikipediaPagesKeyTitle
|
||||
*
|
||||
* Object to store pages from Wikipedia:
|
||||
* - Key: page title sanitized - The real title shown is this title with underscores (_) converted to spaces ( ).
|
||||
* - Value: page id.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, number>} WikipediaPagesKeyId
|
||||
*
|
||||
* Object to store pages from Wikipedia:
|
||||
* - Key: page id.
|
||||
* - Value: page title sanitized - The real title shown is this title with underscores (_) converted to spaces ( ).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef WikipediaInternalLink
|
||||
* @property {number} fromPageId
|
||||
* @property {number} toPageId
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to clean the `page.sql` file by:
|
||||
* - Removing all lines that don't start with `INSERT INTO...`.
|
||||
* - Filter by keeping rows where `page_namespace` (2nd column) is equal to 0, and where `page_is_redirect` (4th column) is equal to false (0).
|
||||
* - Only keep columns `page_id` (1st column) and `page_title` (3rd column).
|
||||
* @returns {Promise<WikipediaPagesKeyId>}
|
||||
*/
|
||||
const cleanPagesSQL = async () => {
|
||||
/** @type {WikipediaPagesKeyId} */
|
||||
const wikipediaPagesKeyId = {}
|
||||
|
||||
const INSERT_INTO_START_INPUT = "INSERT INTO `page` VALUES "
|
||||
const sqlInputPath = path.join(SQL_DUMP_PATH, "page.sql")
|
||||
const sqlInputStat = await fs.promises.stat(sqlInputPath)
|
||||
const sqlInputFileStream = fs.createReadStream(sqlInputPath, "utf-8")
|
||||
|
||||
let isInsideInsert = false
|
||||
let current = ""
|
||||
let lastPercent = 0
|
||||
|
||||
let pagesFileCount = 1
|
||||
|
||||
const INSERT_INTO_START_OUTPUT = "INSERT INTO pages (id, title) VALUES "
|
||||
|
||||
const BATCH_SIZE = 1_000_000
|
||||
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
let batch = []
|
||||
|
||||
const flushBatch = async () => {
|
||||
if (batch.length > 0) {
|
||||
const batchString = batch.join(",")
|
||||
const fileName = `${zeroPad(pagesFileCount, SQL_FILENAME_NUMBER_PAD)}-pages-inserts.sql`
|
||||
const sqlOutputPath = path.join(
|
||||
process.cwd(),
|
||||
"sql-pages-inserts",
|
||||
fileName,
|
||||
)
|
||||
await fs.promises.writeFile(
|
||||
sqlOutputPath,
|
||||
`${INSERT_INTO_START_OUTPUT}${batchString};`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
},
|
||||
)
|
||||
console.log(`flushBatch - ${fileName}, batch.length: ${batch.length}`)
|
||||
pagesFileCount += 1
|
||||
batch = []
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
sqlInputFileStream
|
||||
.on("data", async (dataInput) => {
|
||||
const bytesReadRatio = sqlInputFileStream.bytesRead / sqlInputStat.size
|
||||
const bytesReadPercent = bytesReadRatio * 100
|
||||
|
||||
if (bytesReadPercent - lastPercent >= 1) {
|
||||
console.log(
|
||||
`cleanPagesSQL - Bytes read (${bytesReadPercent.toFixed(2)}%): ${sqlInputFileStream.bytesRead} / ${sqlInputStat.size}`,
|
||||
)
|
||||
lastPercent = bytesReadPercent
|
||||
}
|
||||
|
||||
let data = current + dataInput
|
||||
|
||||
if (!isInsideInsert) {
|
||||
const lines = data.split("\n").filter((line) => {
|
||||
return line.startsWith(INSERT_INTO_START_INPUT)
|
||||
})
|
||||
const [line] = lines
|
||||
if (line == null) {
|
||||
sqlInputFileStream.close()
|
||||
return reject(new Error(`No "${INSERT_INTO_START_INPUT}" found.`))
|
||||
}
|
||||
isInsideInsert = true
|
||||
const lineStripped = line.slice(INSERT_INTO_START_INPUT.length)
|
||||
data = lineStripped
|
||||
}
|
||||
|
||||
const { rows, unCompleted } = extractRowsFromSQLValues(data)
|
||||
current = unCompleted
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.length !== 12) {
|
||||
sqlInputFileStream.close()
|
||||
console.error([row])
|
||||
return reject(new Error(`Invalid Row values.`))
|
||||
}
|
||||
|
||||
const id = Number.parseInt(row[0] ?? "0", 10)
|
||||
const namespace = row[1] ?? ""
|
||||
const title = row[2] ?? ""
|
||||
const isRedirect = row[3] === "1"
|
||||
|
||||
if (namespace === "0" && !isRedirect) {
|
||||
wikipediaPagesKeyId[id] = title
|
||||
batch.push(`(${id},E${title})`)
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length >= BATCH_SIZE) {
|
||||
sqlInputFileStream.pause()
|
||||
await flushBatch()
|
||||
sqlInputFileStream.resume()
|
||||
}
|
||||
})
|
||||
.on("error", (error) => {
|
||||
return reject(error)
|
||||
})
|
||||
.on("close", async () => {
|
||||
await flushBatch()
|
||||
console.log("cleanPagesSQL - Bytes read (100%).")
|
||||
return resolve(wikipediaPagesKeyId)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const wikipediaPagesKeyId = await cleanPagesSQL()
|
||||
|
||||
/**
|
||||
* Function to clean the `pagelinks.sql` file by:
|
||||
* - Removing all lines that don't start with `INSERT INTO...`.
|
||||
* - Filter by keeping rows where `pl_from_namespace` (2nd column) is equal to 0.
|
||||
* - Transform the rows to internal links with fromPageId and toPageId.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const cleanInternalLinksSQL = async () => {
|
||||
let internalLinksFileCount = 1
|
||||
const INSERT_INTO_START_OUTPUT =
|
||||
"INSERT INTO internal_links (from_page_id, to_page_id) VALUES "
|
||||
|
||||
/**
|
||||
* @type {WikipediaPagesKeyTitle}
|
||||
*/
|
||||
const wikipediaPagesKeyTitle = swapKeysAndValues(wikipediaPagesKeyId)
|
||||
|
||||
const INSERT_INTO_START_INPUT = "INSERT INTO `pagelinks` VALUES "
|
||||
const sqlInputPath = path.join(SQL_DUMP_PATH, "pagelinks.sql")
|
||||
const sqlInputStat = await fs.promises.stat(sqlInputPath)
|
||||
const sqlInputFileStream = fs.createReadStream(sqlInputPath, "utf-8")
|
||||
|
||||
let isInsideInsert = false
|
||||
let current = ""
|
||||
let lastPercent = 0
|
||||
|
||||
const BATCH_SIZE = 4_000_000
|
||||
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
let batch = []
|
||||
|
||||
const flushBatch = async () => {
|
||||
if (batch.length > 0) {
|
||||
const batchString = batch.join(",")
|
||||
const fileName = `${zeroPad(internalLinksFileCount, SQL_FILENAME_NUMBER_PAD)}-internal-links-inserts.sql`
|
||||
const sqlOutputPath = path.join(
|
||||
process.cwd(),
|
||||
"sql-internal-links-inserts",
|
||||
fileName,
|
||||
)
|
||||
await fs.promises.writeFile(
|
||||
sqlOutputPath,
|
||||
`${INSERT_INTO_START_OUTPUT}${batchString};`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
},
|
||||
)
|
||||
console.log(`flushBatch - ${fileName}, batch.length: ${batch.length}`)
|
||||
internalLinksFileCount += 1
|
||||
batch = []
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
sqlInputFileStream
|
||||
.on("data", async (dataInput) => {
|
||||
const bytesReadRatio = sqlInputFileStream.bytesRead / sqlInputStat.size
|
||||
const bytesReadPercent = bytesReadRatio * 100
|
||||
|
||||
if (bytesReadPercent - lastPercent >= 0.5) {
|
||||
console.log(
|
||||
`cleanInternalLinksSQL - Bytes read (${bytesReadPercent.toFixed(2)}%): ${sqlInputFileStream.bytesRead} / ${sqlInputStat.size}`,
|
||||
)
|
||||
lastPercent = bytesReadPercent
|
||||
}
|
||||
|
||||
let data = current + dataInput
|
||||
|
||||
if (!isInsideInsert) {
|
||||
const lines = data.split("\n").filter((line) => {
|
||||
return line.startsWith(INSERT_INTO_START_INPUT)
|
||||
})
|
||||
const [line] = lines
|
||||
if (line == null) {
|
||||
sqlInputFileStream.close()
|
||||
return reject(new Error(`No "${INSERT_INTO_START_INPUT}" found.`))
|
||||
}
|
||||
isInsideInsert = true
|
||||
const lineStripped = line.slice(INSERT_INTO_START_INPUT.length)
|
||||
data = lineStripped
|
||||
}
|
||||
|
||||
const { rows, unCompleted } = extractRowsFromSQLValues(data)
|
||||
current = unCompleted
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.length !== 5) {
|
||||
sqlInputFileStream.close()
|
||||
console.error([row])
|
||||
return reject(new Error(`Invalid Row values.`))
|
||||
}
|
||||
|
||||
const plFromPageId = Number.parseInt(row[0] ?? "0", 10)
|
||||
const plTargetNamespace = row[1] ?? ""
|
||||
const plTargetTitle = row[2] ?? ""
|
||||
const plFromNamespace = row[3] ?? ""
|
||||
|
||||
if (plFromNamespace === "0" && plTargetNamespace === "0") {
|
||||
const toPageId = wikipediaPagesKeyTitle[plTargetTitle]
|
||||
if (toPageId != null && wikipediaPagesKeyId[plFromPageId] != null) {
|
||||
/**
|
||||
* @type {WikipediaInternalLink}
|
||||
*/
|
||||
const wikipediaInternalLink = {
|
||||
fromPageId: plFromPageId,
|
||||
toPageId,
|
||||
}
|
||||
batch.push(
|
||||
`(${wikipediaInternalLink.fromPageId},${wikipediaInternalLink.toPageId})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length >= BATCH_SIZE) {
|
||||
sqlInputFileStream.pause()
|
||||
await flushBatch()
|
||||
sqlInputFileStream.resume()
|
||||
}
|
||||
})
|
||||
.on("error", (error) => {
|
||||
return reject(error)
|
||||
})
|
||||
.on("close", async () => {
|
||||
await flushBatch()
|
||||
console.log("cleanInternalLinksSQL - Bytes read (100%).")
|
||||
return resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await cleanInternalLinksSQL()
|
6
data/sql-internal-links-inserts/0000-internal-links.sh
Executable file
6
data/sql-internal-links-inserts/0000-internal-links.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for sqlInsert in /data/sql-internal-links-inserts/*.sql; do
|
||||
echo "${sqlInsert}"
|
||||
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="${sqlInsert}"
|
||||
done
|
6
data/sql-pages-inserts/0000-pages.sh
Executable file
6
data/sql-pages-inserts/0000-pages.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for sqlInsert in /data/sql-pages-inserts/*.sql; do
|
||||
echo "${sqlInsert}"
|
||||
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="${sqlInsert}"
|
||||
done
|
3
data/sql/0000-sql-init.sh
Executable file
3
data/sql/0000-sql-init.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="/data/sql/0000-sql-init.sql"
|
2
data/sql/0000-sql-init.sql
Normal file
2
data/sql/0000-sql-init.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE pages DISABLE TRIGGER ALL;
|
||||
ALTER TABLE internal_links DISABLE TRIGGER ALL;
|
3
data/sql/0999-sql-end.sh
Executable file
3
data/sql/0999-sql-end.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="/data/sql/0999-sql-end.sql"
|
2
data/sql/0999-sql-end.sql
Normal file
2
data/sql/0999-sql-end.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE pages ENABLE TRIGGER ALL;
|
||||
ALTER TABLE internal_links ENABLE TRIGGER ALL;
|
88
data/utils.js
Normal file
88
data/utils.js
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Extracts rows from a string of values in a SQL INSERT INTO statement, where each row is a comma-separated list of values enclosed in parentheses, possibly with the last row incomplete.
|
||||
* @param {string} input
|
||||
* @returns {{rows: string[][], unCompleted:string}}
|
||||
* @example extractRowsFromSQLValues("(1,'-)',0),(2,'Demographics_of_American_Samoa',0)") // { rows: [["1","'-)'","0"],["2","'Demographics_of_American_Samoa'","0"]], unCompleted: "" }
|
||||
*/
|
||||
export const extractRowsFromSQLValues = (input) => {
|
||||
const rows = []
|
||||
let index = 0
|
||||
let unCompleted = ""
|
||||
|
||||
while (index < input.length) {
|
||||
if (input[index] === "(") {
|
||||
const row = []
|
||||
index++ // Skip the opening '('
|
||||
let value = ""
|
||||
let insideQuotes = false
|
||||
let rowComplete = false
|
||||
|
||||
while (index < input.length && !rowComplete) {
|
||||
if (input[index] === "'") {
|
||||
// An escaped quote is preceded by an odd number of backslashes.
|
||||
let backslashCount = 0
|
||||
let backIndex = index - 1
|
||||
while (backIndex >= 0 && input[backIndex] === "\\") {
|
||||
backslashCount++
|
||||
backIndex--
|
||||
}
|
||||
if (backslashCount % 2 === 0) {
|
||||
insideQuotes = !insideQuotes
|
||||
}
|
||||
}
|
||||
|
||||
if (input[index] === "," && !insideQuotes) {
|
||||
row.push(value)
|
||||
value = ""
|
||||
} else if (input[index] === ")" && !insideQuotes) {
|
||||
row.push(value)
|
||||
rows.push(row)
|
||||
rowComplete = true
|
||||
} else {
|
||||
value += input[index]
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
if (!rowComplete) {
|
||||
// If row is not completed, save it to unCompleted
|
||||
unCompleted = "("
|
||||
if (row.length > 0) {
|
||||
unCompleted += row.join(",") + "," + value
|
||||
} else if (value.length > 0) {
|
||||
unCompleted += value
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
return { rows, unCompleted }
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the keys and values of an object.
|
||||
* @param {*} object
|
||||
* @returns
|
||||
*/
|
||||
export const swapKeysAndValues = (object) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object).map(([key, value]) => {
|
||||
return [value, key]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} number
|
||||
* @param {number} places
|
||||
* @returns {string}
|
||||
* @example zeroPad(1, 2) // '01'
|
||||
* @example zeroPad(10, 2) // '10'
|
||||
*/
|
||||
export const zeroPad = (number, places = 2) => {
|
||||
return number.toString().padStart(places, "0")
|
||||
}
|
21
package.json
21
package.json
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "repo",
|
||||
"version": "1.0.0-staging.1",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903",
|
||||
"packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=9.5.0"
|
||||
"pnpm": ">=9.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"start": "turbo run start --parallel",
|
||||
"database:migrate": "turbo run database:migrate",
|
||||
"build": "turbo run build",
|
||||
"test": "turbo run test",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:prettier": "prettier . --check",
|
||||
@ -24,11 +25,19 @@
|
||||
"@semantic-release/exec": "6.0.3",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"editorconfig-checker": "5.1.8",
|
||||
"playwright": "catalog:",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "0.6.5",
|
||||
"prettier-plugin-tailwindcss": "0.6.6",
|
||||
"replace-in-files-cli": "3.0.0",
|
||||
"semantic-release": "23.1.1",
|
||||
"turbo": "2.0.9",
|
||||
"turbo": "2.1.0",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@tuyau/core@0.1.4": "patches/@tuyau__core@0.1.4.patch",
|
||||
"@tuyau/openapi@0.2.0": "patches/@tuyau__openapi@0.2.0.patch",
|
||||
"@tuyau/client@0.1.2": "patches/@tuyau__client@0.1.2.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
packages/api-client/.eslintrc.json
Normal file
14
packages/api-client/.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@repo/eslint-config"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
25
packages/api-client/package.json
Normal file
25
packages/api-client/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@repo/api-client",
|
||||
"version": "1.0.0-staging.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/api.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||
"lint:typescript": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tuyau/client": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/config-typescript": "workspace:*",
|
||||
"@repo/api": "workspace:",
|
||||
"@types/node": "catalog:",
|
||||
"@total-typescript/ts-reset": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
11
packages/api-client/src/api.ts
Normal file
11
packages/api-client/src/api.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference path="../../../apps/api/adonisrc.ts" />
|
||||
|
||||
import type { ApiDefinition } from "@repo/api"
|
||||
import { createTuyau } from "@tuyau/client"
|
||||
|
||||
export const api = createTuyau<{ definition: ApiDefinition }>({
|
||||
baseUrl:
|
||||
process.env["API_URL"] ??
|
||||
process.env["NEXT_PUBLIC_API_URL"] ??
|
||||
"http://127.0.0.1:5500",
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user