Compare commits

...

29 Commits

Author SHA1 Message Date
semantic-release-bot
4cfb73b2d4
chore(release): 1.0.0-staging.4 [skip ci] 2024-08-30 18:30:16 +00:00
7ecfb97df5
docs: rework wording status of the project
All checks were successful
Chromatic / chromatic (push) Successful in 1m47s
CI / ci (push) Successful in 4m27s
CI / commitlint (push) Successful in 14s
Release / release (push) Successful in 56s
2024-08-30 20:18:56 +02:00
bb39ae856d
docs: add notes about status of the project (abandoned for the moment)
All checks were successful
Chromatic / chromatic (push) Successful in 2m30s
CI / ci (push) Successful in 4m40s
CI / commitlint (push) Successful in 19s
2024-08-30 20:14:50 +02:00
170bdae725
build(deps): update latest 2024-08-30 20:06:53 +02:00
eba92ed64b
chore(storybook): next-intl Locale type 2024-08-25 02:00:02 +02:00
92787448bb
docs: database migrations 2024-08-25 01:50:29 +02:00
bf1729cf0d
perf(docker): optimize pnpm installation 2024-08-25 01:39:16 +02:00
f0b22f6a06
feat(api): work in progress GET /wikipedia/shortest-paths?fromPageId=id&toPageId=id 2024-08-23 23:21:42 +02:00
4e707008f8
chore(api): improve DX (Developer Experience)
All checks were successful
Chromatic / chromatic (push) Successful in 1m55s
CI / ci (push) Successful in 4m33s
CI / commitlint (push) Successful in 15s
2024-08-19 22:11:33 +01:00
20ab889cf8
chore: improve type safety Tuyau
All checks were successful
Chromatic / chromatic (push) Successful in 2m3s
CI / ci (push) Successful in 4m16s
CI / commitlint (push) Successful in 14s
2024-08-18 01:31:02 +01:00
4add77856e
chore: try Adonis Tuyau
All checks were successful
Chromatic / chromatic (push) Successful in 2m58s
CI / ci (push) Successful in 4m43s
CI / commitlint (push) Successful in 15s
2024-08-16 01:50:11 +01:00
791551a4e8
chore: simplify TypeScript config
All checks were successful
Chromatic / chromatic (push) Successful in 2m44s
CI / ci (push) Successful in 4m0s
CI / commitlint (push) Successful in 14s
2024-08-15 14:14:21 +01:00
63862b19c0
feat(api): implement GET /wikipedia/pages/[id]
Some checks failed
Chromatic / chromatic (push) Successful in 2m33s
CI / ci (push) Failing after 1m27s
CI / commitlint (push) Successful in 13s
2024-08-13 11:58:38 +01:00
376c0fd041
feat(api): health checks 2024-08-12 18:19:43 +01:00
207d3483ca
feat(api): rate limiting 2024-08-12 14:13:24 +01:00
0ce950c9c8
test(api): database tests 2024-08-12 13:21:34 +01:00
cdc8cf2b05
feat(api): implement GET /wikipedia/pages?title=search_title 2024-08-12 00:32:43 +01:00
02ee112de4
feat(api): create Lucid models and migrations for Wikipedia database dump + usage of PostgreSQL instead of MariaDB 2024-08-11 09:49:23 +01:00
aa2fb4f5b9
chore: fix usage issues
All checks were successful
Chromatic / chromatic (push) Successful in 5m54s
CI / ci (push) Successful in 4m24s
CI / commitlint (push) Successful in 18s
2024-08-10 01:32:34 +01:00
0f8b6c6b29
chore: fix tailwindcss content pattern matching node_modules
Some checks failed
Chromatic / chromatic (push) Successful in 6m2s
CI / ci (push) Failing after 54s
CI / commitlint (push) Successful in 15s
Ref: https://x.com/adamwathan/status/1821286325259239554
2024-08-09 23:04:38 +01:00
94af8462d3
chore: stricter tsconfig.json 2024-08-09 22:52:02 +01:00
d020552af5
feat: init apps/api 2024-08-09 22:51:41 +01:00
f53a797169
feat: wikipedia data dump working 2024-08-08 02:21:53 +01:00
8dec198afe
fix: wikipedia data dump POC improvements 2024-08-07 00:21:08 +01:00
3bed3e0578
perf: improve memory usage for POC to get wikipedia dump 2024-08-05 17:36:19 +02:00
fee0b4e681
feat: adapt internal_links SQL file POC 2024-08-05 14:04:28 +02:00
61914d2392
chore: clean up POC to get Wikipedia dump 2024-08-05 00:52:48 +02:00
3de838dded
feat: start implementation to get Wikipedia dump and customize it 2024-08-05 00:37:06 +02:00
91e1f18575
build(deps): update latest
All checks were successful
Chromatic / chromatic (push) Successful in 5m23s
CI / ci (push) Successful in 3m55s
CI / commitlint (push) Successful in 22s
2024-08-02 00:50:10 +02:00
153 changed files with 9338 additions and 1677 deletions

View File

@ -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

View File

@ -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"

13
.gitignore vendored
View File

@ -20,8 +20,15 @@ build/
.DS_Store
*.pem
.turbo
bin/
cache.json
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*
@ -51,4 +58,4 @@ storybook-static
# typescript
*.tsbuildinfo
# next-env.d.ts
next-env.d.ts

36
.vscode/adonis.code-snippets vendored Normal file
View 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",
},
}

View File

@ -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}\",",

View File

@ -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

55
TODO.md
View File

@ -1,13 +1,56 @@
# TODO
- [x] chore: initial commit (+ mirror on GitHub)
- [x] chore: initial commit
- [x] Deploy first staging version (v1.0.0-staging.1)
- [ ] Implement Wikipedia Game Solver (`website`) with inputs, button to submit, and list all pages to go from one to another, or none if it is not possible
- [ ] Check, cache and store (in `.json` file) all Wikipedia Pages and its internal links, maybe use Wikipedia Dump (<https://en.wikipedia.org/wiki/Wikipedia:Database_download>)?
- [ ] Implement toast notifications for errors, warnings, and success messages
- [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`)
- [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/))
- [ ] 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.
- [ ] 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

4
apps/api/.c8rc.json Normal file
View File

@ -0,0 +1,4 @@
{
"reporter": ["text", "html", "json"],
"exclude": ["adonisrc.ts", "tests/**", "database/**", "config/**", "bin/**"]
}

16
apps/api/.env.example Normal file
View 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
View 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
View 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
View 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,
},
})

View File

@ -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)
})
})

View 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"],
})

View File

@ -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)
})
})

View File

@ -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"],
})

View File

@ -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)
})
})

View File

@ -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"],
})

View File

@ -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"],
})

View 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)
}
}

View 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" })
}
}

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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[]
}

View 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
View 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
View 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
View 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
View 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
View 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> {}
}

View 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
View 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

View 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
View 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> {}
}

View 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
View 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
View 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

View 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()

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
View 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:"
}
}

View 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
},
),
)

View File

@ -0,0 +1,3 @@
import { BaseModel, CamelCaseNamingStrategy } from "@adonisjs/lucid/orm"
BaseModel.namingStrategy = new CamelCaseNamingStrategy()

37
apps/api/start/env.ts Normal file
View 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
View 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
View 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
View 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
View 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)
}),
)

View 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
View File

@ -0,0 +1,7 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext"],
"types": ["@total-typescript/ts-reset", "@types/node"]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@repo/cli",
"version": "1.0.0-staging.3",
"version": "1.0.0-staging.4",
"private": true,
"type": "module",
"imports": {
@ -11,7 +11,6 @@
},
"scripts": {
"start": "node --import=tsx ./src/index.ts",
"dev-test": "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"
},

View File

@ -1,11 +1,9 @@
#!/usr/bin/env -S node --import=tsx
import { add } from "#abc/def/add.js"
import { add } from "#abc/def/add.ts"
import { VERSION } from "@repo/utils/constants"
import { sum } from "@repo/wikipedia-game-solver/wikipedia-api"
console.log("Hello, world!")
console.log(sum(1, 2))
console.log(add(2, 3))
console.log(`v${VERSION}`)

View File

@ -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"]
}
}

View File

@ -1,6 +1,7 @@
import "@repo/config-tailwind/styles.css"
import { defaultTranslationValues, Locale } from "@repo/i18n/config"
import { defaultTranslationValues } from "@repo/i18n/config"
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
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"

View File

@ -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) {

View File

@ -1,6 +1,6 @@
{
"name": "@repo/storybook",
"version": "1.0.0-staging.3",
"version": "1.0.0-staging.4",
"private": true,
"type": "module",
"scripts": {
@ -16,6 +16,7 @@
"@repo/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/wikipedia-game-solver": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",

View File

@ -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],
}

View File

@ -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

View File

@ -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.10
COPY ./ ./
RUN pnpm install --global turbo@2.1.0
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer

View File

@ -1,10 +1,10 @@
import "@repo/config-tailwind/styles.css"
import type { Locale, LocaleProps } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config"
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 { VERSION } from "@repo/utils/constants"
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 {
@ -59,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

View File

@ -2,10 +2,7 @@ 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/Layout/MainLayout"
import {
fromLocaleToWikipediaLocale,
getWikipediaLink,
} from "@repo/wikipedia-game-solver/wikipedia-api"
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"

View File

@ -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({

View File

@ -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.

View File

@ -1,6 +1,6 @@
{
"name": "@repo/website",
"version": "1.0.0-staging.3",
"version": "1.0.0-staging.4",
"private": true,
"type": "module",
"imports": {
@ -19,6 +19,7 @@
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/wikipedia-game-solver": "workspace:*",
"@repo/wikipedia": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",

View File

@ -2,7 +2,7 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = {
content: ["./**/*.tsx", "../../packages/**/*.tsx"],
content: ["./app/**/*.tsx", "../../packages/*/src/**/*.tsx"],
presets: [sharedConfig],
}

View File

@ -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
View 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:

View File

@ -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
View 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"
}
]
}
}

View File

@ -1,17 +1,163 @@
# Wikipedia data
Database layout: <https://www.mediawiki.org/wiki/Manual:Database_layout>
```sh
./download-wikipedia-dump.sh
node --max-old-space-size=8096 generate-sql-files.js
<https://stackoverflow.com/questions/43954631/issues-with-wikipedia-dump-table-pagelinks>
# Inside the Database container
docker exec -it wikipedia-solver-dev-database sh
/data/execute-sql.sh
```
<https://stackoverflow.com/questions/40384864/importing-wikipedia-dump-to-mysql>
## 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/>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pagelinks.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-page.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles-in-ns0.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-iwlinks.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles.gz>
```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`
--
```

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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="&#xd;" horiz-adv-x="681" />
<glyph unicode="&#x2000;" horiz-adv-x="513" />
<glyph unicode="&#x2001;" horiz-adv-x="1026" />
<glyph unicode="&#x2002;" horiz-adv-x="513" />
<glyph unicode="&#x2003;" horiz-adv-x="1026" />
<glyph unicode="&#x2004;" horiz-adv-x="342" />
<glyph unicode="&#x2005;" horiz-adv-x="256" />
<glyph unicode="&#x2006;" horiz-adv-x="171" />
<glyph unicode="&#x2007;" horiz-adv-x="171" />
<glyph unicode="&#x2008;" horiz-adv-x="128" />
<glyph unicode="&#x2009;" horiz-adv-x="205" />
<glyph unicode="&#x200a;" horiz-adv-x="57" />
<glyph unicode="&#x2016;" 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="&#x202f;" horiz-adv-x="205" />
<glyph unicode="&#x205f;" horiz-adv-x="256" />
<glyph unicode="&#x2139;" 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="&#x2190;" horiz-adv-x="1208" d="M348 256l-246 256l246 256v-164h758v-182h-758v-166z" />
<glyph unicode="&#x2191;" horiz-adv-x="716" d="M614 770h-165v-760h-181v760h-166l256 244z" />
<glyph unicode="&#x2192;" horiz-adv-x="1208" d="M862 256v166h-760v182h760v164l244 -256z" />
<glyph unicode="&#x2193;" horiz-adv-x="716" d="M614 256l-256 -246l-256 246h166v758h181v-758h165z" />
<glyph unicode="&#x21b0;" horiz-adv-x="1075" d="M307 512v-92l-205 164l205 174v-103h563q41 0 72 -29.5t31 -72.5v-287h-144v246h-522z" />
<glyph unicode="&#x21b3;" horiz-adv-x="966" d="M205 358q-43 0 -73 31t-30 72v358h144v-317h372v153l246 -225l-246 -225v153h-413z" />
<glyph unicode="&#x21c6;" d="M819 760v-144h-512v-92l-205 164l205 174v-102h512zM1126 330l-204 -164v92h-512v143h512v103z" />
<glyph unicode="&#x221e;" 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="&#x2295;" 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="&#x2296;" 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="&#x229e;" 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="&#x229f;" 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="&#x2302;" 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="&#x2328;" 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="&#x232b;" 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="&#x23e9;" 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="&#x23ea;" 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="&#x23ed;" 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="&#x23ee;" 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="&#x23f3;" 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="&#x23f4;" 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="&#x23f5;" 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="&#x23f6;" 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="&#x23f7;" 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="&#x25a0;" 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="&#x25b4;" horiz-adv-x="675" d="M102 307l236 410l235 -410h-471z" />
<glyph unicode="&#x25b6;" 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="&#x25b8;" horiz-adv-x="614" d="M102 748l410 -236l-410 -236v472z" />
<glyph unicode="&#x25be;" horiz-adv-x="675" d="M573 717l-235 -410l-236 410h471z" />
<glyph unicode="&#x25c2;" horiz-adv-x="614" d="M512 748v-472l-410 236z" />
<glyph unicode="&#x25cf;" 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="&#x25d1;" 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="&#x25f4;" 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="&#x25fc;" horiz-adv-x="1024" d="M0 0v0v0v0v0z" />
<glyph unicode="&#x2601;" 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="&#x2605;" horiz-adv-x="1105" d="M553 963l123 -345h328l-269 -200l96 -357l-278 213l-279 -213l97 357l-269 200h328z" />
<glyph unicode="&#x2606;" 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="&#x2615;" 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="&#x2630;" 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="&#x263d;" 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="&#x2661;" 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="&#x2665;" 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="&#x266a;" 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="&#x266b;" 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="&#x268f;" 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="&#x2691;" 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="&#x2692;" 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="&#x2699;" 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="&#x26a0;" 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="&#x26a1;" 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="&#x26c8;" 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="&#x2707;" 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="&#x2708;" 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="&#x2709;" 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="&#x270e;" 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="&#x2712;" 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="&#x2713;" 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="&#x2716;" 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="&#x274c;" 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="&#x274e;" 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="&#x2753;" 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="&#x275e;" 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="&#x2795;" 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="&#x2796;" horiz-adv-x="798" d="M666 563q31 0 30.5 -51t-30.5 -51h-533q-31 0 -31 51t31 51h533z" />
<glyph unicode="&#x27a1;" horiz-adv-x="952" d="M461 850l389 -338l-389 -338v197h-359v284h359v195z" />
<glyph unicode="&#x27a2;" 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="&#x27a6;" 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="&#x27f2;" 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="&#x27f3;" 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="&#x2b05;" horiz-adv-x="952" d="M489 174l-387 338l387 338v-195h361v-284h-361v-197z" />
<glyph unicode="&#x2b06;" horiz-adv-x="880" d="M778 498h-196v-359h-283v359h-197l338 389z" />
<glyph unicode="&#x2b07;" horiz-adv-x="880" d="M778 528l-338 -389l-338 389h197v359h283v-359h196z" />
<glyph unicode="&#xe003;" 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="&#xe005;" 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="&#xe4ad;" horiz-adv-x="952" d="M489 901v-194h361v-388h-361v-196l-387 389z" />
<glyph unicode="&#xe4ae;" horiz-adv-x="952" d="M461 901l389 -389l-389 -389v196h-359v388h359v194z" />
<glyph unicode="&#xe4af;" horiz-adv-x="983" d="M881 498h-197v-359h-385v359h-197l390 389z" />
<glyph unicode="&#xe4b0;" horiz-adv-x="983" d="M881 528l-389 -389l-390 389h197v359h385v-359h197z" />
<glyph unicode="&#xe700;" 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="&#xe704;" 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="&#xe705;" 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="&#xe70a;" 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="&#xe70c;" 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="&#xe711;" 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="&#xe712;" 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="&#xe713;" 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="&#xe714;" 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="&#xe715;" 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="&#xe716;" 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="&#xe717;" 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="&#xe718;" 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="&#xe720;" 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="&#xe722;" 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="&#xe723;" 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="&#xe724;" 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="&#xe727;" 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="&#xe728;" 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="&#xe729;" 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="&#xe730;" 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="&#xe731;" 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="&#xe736;" 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="&#xe737;" 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="&#xe73a;" 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="&#xe73c;" 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="&#xe73d;" 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="&#xe73e;" 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="&#xe740;" 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="&#xe741;" 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="&#xe744;" horiz-adv-x="1015" d="M590 918h323v-324l-102 127l-149 -156l-103 103l156 149zM354 463l103 -103l-156 -149l125 -102h-324v323l103 -125z" />
<glyph unicode="&#xe746;" 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="&#xe74c;" 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="&#xe74d;" 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="&#xe74e;" 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="&#xe74f;" horiz-adv-x="675" d="M338 1024l235 -373h-471zM338 0l-236 375h471z" />
<glyph unicode="&#xe758;" 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="&#xe759;" 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="&#xe75a;" 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="&#xe75b;" 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="&#xe75c;" 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="&#xe75d;" 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="&#xe75e;" 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="&#xe75f;" 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="&#xe760;" 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="&#xe761;" 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="&#xe762;" 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="&#xe763;" 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="&#xe764;" 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="&#xe765;" 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="&#xe766;" 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="&#xe767;" 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="&#xe768;" 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="&#xe769;" 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="&#xe76a;" 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="&#xe76b;" 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="&#xe771;" 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="&#xe776;" 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="&#xe777;" 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="&#xe778;" 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="&#xe788;" 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="&#xe789;" 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="&#xe79a;" 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="&#xe7a1;" 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="&#xe800;" 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="&#xf020;" horiz-adv-x="1024" />
<glyph unicode="&#xf601;" 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="&#x1f304;" 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="&#x1f30e;" 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="&#x1f342;" 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="&#x1f393;" 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="&#x1f394;" 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="&#x1f3a4;" 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="&#x1f3a8;" 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="&#x1f3ab;" 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="&#x1f3ac;" 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="&#x1f3af;" 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="&#x1f3b5;" 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="&#x1f3c6;" 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="&#x1f44d;" 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="&#x1f44e;" 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="&#x1f45c;" 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="&#x1f464;" 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="&#x1f465;" 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="&#x1f4a1;" 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="&#x1f4a5;" 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="&#x1f4a6;" 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="&#x1f4a7;" 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="&#x1f4a8;" 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="&#x1f4b3;" 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="&#x1f4bb;" 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="&#x1f4bc;" 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="&#x1f4be;" 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="&#x1f4bf;" 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="&#x1f4c1;" 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="&#x1f4c4;" 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="&#x1f4c5;" 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="&#x1f4c8;" 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="&#x1f4ca;" 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="&#x1f4cb;" 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="&#x1f4ce;" 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="&#x1f4d1;" 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="&#x1f4d5;" 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="&#x1f4d6;" 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="&#x1f4de;" 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="&#x1f4e3;" 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="&#x1f4e4;" 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="&#x1f4e5;" 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="&#x1f4e6;" 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="&#x1f4f0;" 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="&#x1f4f1;" 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="&#x1f4f6;" 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="&#x1f4f7;" 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="&#x1f4f8;" 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="&#x1f4fd;" 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="&#x1f4fe;" 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="&#x1f4ff;" 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="&#x1f500;" 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="&#x1f501;" 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="&#x1f504;" 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="&#x1f505;" 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="&#x1f506;" 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="&#x1f507;" 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="&#x1f50a;" 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="&#x1f50b;" 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="&#x1f50d;" 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="&#x1f511;" 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="&#x1f512;" 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="&#x1f513;" 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="&#x1f514;" 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="&#x1f516;" horiz-adv-x="573" d="M420 973q23 0 37 -15.5t14 -35.5v-871l-184 185l-185 -185v871q0 51 41 51h277z" />
<glyph unicode="&#x1f517;" 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="&#x1f519;" 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="&#x1f526;" 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="&#x1f53e;" 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="&#x1f53f;" 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="&#x1f545;" 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="&#x1f546;" 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="&#x1f547;" 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="&#x1f548;" 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="&#x1f549;" 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="&#x1f54a;" 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="&#x1f54b;" 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="&#x1f54c;" 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="&#x1f54d;" 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="&#x1f54e;" 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="&#x1f54f;" 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="&#x1f554;" 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="&#x1f568;" 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="&#x1f569;" 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="&#x1f56a;" 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="&#x1f56b;" 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="&#x1f56c;" 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="&#x1f5f9;" 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="&#x1f5fa;" 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="&#x1f680;" 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="&#x1f6ab;" 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="&#x1f6c6;" 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="&#x1f6c7;" 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="&#x1f6c8;" 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

Binary file not shown.

Binary file not shown.

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
View 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
View 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
View 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()

View 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

View 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
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="/data/sql/0000-sql-init.sql"

View 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
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
time psql --username="${DATABASE_USER}" --dbname="${DATABASE_NAME}" --file="/data/sql/0999-sql-end.sql"

View File

@ -0,0 +1,2 @@
ALTER TABLE pages ENABLE TRIGGER ALL;
ALTER TABLE internal_links ENABLE TRIGGER ALL;

88
data/utils.js Normal file
View 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")
}

View File

@ -1,17 +1,18 @@
{
"name": "repo",
"version": "1.0.0-staging.3",
"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",
@ -26,10 +27,17 @@
"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.10",
"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"
}
}
}

View 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
}
}
]
}

View 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:"
}
}

View 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",
})

View File

@ -0,0 +1,7 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext"],
"types": ["@types/node", "@total-typescript/ts-reset"]
}
}

View File

@ -1,3 +1,25 @@
{
"extends": ["conventions"]
"extends": ["conventions"],
"plugins": ["import-x"],
"rules": {
"import-x/no-absolute-path": "error",
"import-x/no-webpack-loader-syntax": "error",
"import-x/no-self-import": "error",
"import-x/no-useless-path-segments": "error",
"import-x/export": "error",
"import-x/no-duplicates": "error",
"import-x/no-named-default": "error",
"import-x/no-empty-named-blocks": "error",
"import-x/no-anonymous-default-export": "error",
"import-x/extensions": [
"error",
"ignorePackages",
{
"ts": "always",
"tsx": "always",
"js": "never",
"jsx": "never"
}
]
}
}

View File

@ -1,6 +1,6 @@
{
"extends": [
"conventions",
"../.eslintrc.json",
"next/core-web-vitals",
"plugin:tailwindcss/recommended",
"plugin:storybook/recommended"

View File

@ -1,6 +1,6 @@
{
"name": "@repo/eslint-config",
"version": "1.0.0-staging.3",
"version": "1.0.0-staging.4",
"private": true,
"main": ".eslintrc.json",
"files": [
@ -17,6 +17,7 @@
"eslint-config-next": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-tailwindcss": "catalog:",
"eslint-plugin-import-x": "catalog:",
"typescript": "catalog:"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@repo/config-tailwind",
"version": "1.0.0-staging.3",
"version": "1.0.0-staging.4",
"private": true,
"type": "module",
"main": "./tailwind.config.js",

View File

@ -1,10 +1,6 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true
"lib": ["ESNext"]
}
}

Some files were not shown because too many files have changed in this diff Show More