Refac/domain driven #1

Merged
fsylvain merged 40 commits from refac/domain-driven into main 2026-09-25 16:15:15 +02:00
418 changed files with 27029 additions and 14050 deletions

No files matched your search

+9 -40
View File
@@ -1,51 +1,20 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp/main.exe"
cmd = "go build -o ./tmp/main.exe ./main"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "api\\static\\tinymce", "api\\static\\bootstrap-icons", "api\\static\\uploadedImages"]
exclude_file = []
cmd = "go build -tags nofrontend -o ./tmp/renewcms.exe ./cmd/server"
bin = "tmp/renewcms.exe"
delay = 500
exclude_dir = ["tmp", "bin", "data", "web", "features", "docs", "internal/infrastructure/assets/dist", ".claude"]
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
include_ext = ["go", "html"]
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
send_interrupt = true
kill_delay = "2s"
stop_on_error = true
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true
clean_on_exit = true
+11 -7
View File
@@ -1,11 +1,15 @@
ENVIRONMENT=development
HOST=example.com
PORT=8080
JWT_SECRET=abc123
CORS_ALLOWED_ORIGINS=*
DB_FILE=./renewcms.db
DOCKER_DB_FOLDER=./data
PUBLIC_URL=http://localhost:5173
JWT_SECRET=change-me-to-a-long-random-string
SMTP_EMAIL=a@b.c
SMTP_PASSWORD=12341234
SMTP_HOST=mail.service.abc
SMTP_PORT=465
ENVIRONMENT=development
PORT=8080
DB_FILE=./data/renewcms.db
UPLOAD_DIR=./data/uploads
CORS_ALLOWED_ORIGINS=
AUTH_RATE_LIMIT_MAX_ATTEMPTS=5
AUTH_RATE_LIMIT_WINDOW_SECONDS=60
TRUSTED_PROXIES=
+4 -1
View File
@@ -1 +1,4 @@
api/static/** linguist-vendored
* text=auto eol=lf
*.ico binary
*.png binary
*.jpg binary
-40
View File
@@ -1,40 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
environment: test
env:
ENVIRONMENT: ${{ secrets.ENVIRONMENT }}
HOST: ${{ secrets.HOST }}
PORT: ${{ secrets.PORT }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
CORS_ALLOWED_ORIGINS: ${{ secrets.CORS_ALLOWED_ORIGINS }}
DB_FILE: ${{ secrets.DB_FILE }}
DOCKER_DB_FOLDER: ${{ secrets.DOCKER_DB_FOLDER }}
SMTP_EMAIL: ${{ secrets.SMTP_EMAIL }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23.0"
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
+7
View File
@@ -4,14 +4,21 @@
# DB
*.db
data/
# Build
bin
dist/
# Environment
.env
# Generated files
tmp/
.task/
build-errors.log
uploadedImages/
*.exe
.claude/
CLAUDE.md
+100
View File
@@ -0,0 +1,100 @@
# Coding Standards
These rules apply to every file in this repo , for humans and agents alike.
## General rules
1. Follow standard conventions.
2. Keep it simple stupid. Simpler is always better. Reduce complexity as much as possible.
3. Boy scout rule. Leave the campground cleaner than you found it.
4. Always find root cause. Always look for the root cause of a problem.
## Design rules
1. Keep configurable data at high levels.
2. Prefer polymorphism to if/else or switch/case.
3. Separate multi-threading code.
4. Prevent over-configurability.
5. Use dependency injection.
6. Follow Law of Demeter. A class should know only its direct dependencies.
## Understandability tips
1. Be consistent. If you do something a certain way, do all similar things in the same way.
2. Use explanatory variables.
3. Encapsulate boundary conditions. Boundary conditions are hard to keep track of. Put the processing for them in one place.
4. Prefer dedicated value objects to primitive types.
5. Avoid logical dependency. Don't write methods which work correctly only depending on something else in the same class.
6. Avoid negative conditionals.
## Names rules
1. Choose descriptive and unambiguous names.
2. Make meaningful distinctions.
3. Use pronounceable names.
4. Use searchable names.
5. Replace magic numbers with named constants.
6. Avoid encodings. Don't append prefixes or type information.
## Functions rules
1. Small.
2. Do one thing.
3. Use descriptive names.
4. Prefer fewer arguments.
5. Have no side effects.
6. Don't use flag arguments. Split the method into several independent methods that can be called from the client without the flag.
## Comments rules
1. No comments in the codebase. This covers Go, TypeScript/Vue, HTML templates and config files (YAML, TOML, `.env.example`, `.gitattributes`, …).
2. Explain yourself in code instead: descriptive names, small functions, named constants, explanatory variables.
3. The only exceptions are tool directives the code needs to work, such as `//go:build` and `//go:embed`.
4. Don't comment out code; just remove it.
5. Prose belongs in `docs/` and `README.md`, not in source files.
## Source code structure
1. Separate concepts vertically.
2. Related code should appear vertically dense.
3. Declare variables close to their usage.
4. Dependent functions should be close.
5. Similar functions should be close.
6. Place functions in the downward direction (caller above callee).
7. Keep lines short.
8. Don't use horizontal alignment.
9. Use white space to associate related things and disassociate weakly related ones.
10. Don't break indentation.
## Objects and data structures
1. Hide internal structure.
2. Prefer data structures when there's no behavior to attach.
3. Avoid hybrid structures (half object, half data).
4. Keep classes small.
5. Each class should do one thing.
6. Keep the number of instance variables small.
7. Base classes should know nothing about their derivatives.
8. Prefer many small functions over one function with a parameter that selects behavior.
9. Prefer non-static methods to static methods.
## Tests
1. One assert per test.
2. Readable.
3. Fast.
4. Independent.
5. Repeatable.
## Code smells to watch for
1. **Rigidity** — the software is difficult to change; a small change causes a cascade of subsequent changes.
2. **Fragility** — the software breaks in many places due to a single change.
3. **Immobility** — you cannot reuse parts of the code in other projects because of the risk and effort involved.
4. **Needless complexity**.
5. **Needless repetition**.
6. **Opacity** — the code is hard to understand.
## Credits
Thanks to [@wojtekluwojteklu/clean_code.md](https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29)
-16
View File
@@ -1,16 +0,0 @@
FROM golang:1.20.5-alpine
WORKDIR /app
COPY go.mod go.sum ./
COPY adapters ./adapters
COPY api ./api
COPY domain ./domain
COPY main ./main
COPY useCases ./useCases
COPY .env ./.env
RUN go mod download
RUN go build -o startServer ./main
CMD ["./startServer"]
+22 -45
View File
@@ -1,66 +1,43 @@
# RenewCMS
## 🚧 This project is under development !
A small self-hosted CMS: a Go API and a Vue 3 admin/front SPA, shipped as **a single executable** backed by a SQLite file.
The features are not complete, not fully tested and subject to many changes.
> 🚧 **Under development.** Features are incomplete, not fully tested and subject to change.
## Setup
## Quick start
Don't forget to setup the [Environment variables](#environment-variables)!
### Run a release binary
### Use with Docker
#### Compose
The easiest way to run the project is to use docker-compose.
You still need to set up the environment variables, but you can use the `.env` file for that.
Note that some supplementary variables are required for docker-compose to work, like `DOCKER_DB_FOLDER`.
```bash
docker compose up --build -d
```sh
cp .env.example .env # fill in PUBLIC_URL, JWT_SECRET and SMTP_*
./renewcms # → http://localhost:8080
```
### Build and run it yourself
Data is stored in `./data/` by default. For a real server (systemd, HTTPS, backups), see **[docs/deployment.md](docs/deployment.md)**.
#### Build
### Develop
There are 2 scripts to build the project:
Requires Go 1.27+, Node.js 22, [Bun](https://bun.sh) and [Task](https://taskfile.dev).
- `super_build.sh`
- `super_build.bat`
```sh
cp .env.example .env
task setup
task dev # API with hot reload + Vite HMR → http://localhost:5173
```
They will build the project for windows, linux and macOS. The binaries will be in the `bin` folder.
You can also really just use `go build` to build the project for your current OS.
## Documentation
#### Run
Once the project is built, you can simply run the binary for your OS. Setting the environment variables is
of course required, but not necessarily via the `.env` file.
### Environment variables
| Name | Type | Description | Comment |
|----------------------|--------|:----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| ENVIRONMENT | string | environment the API is running in | required, `development` or `production` |
| HOST | string | the host domain name for emails and callbacks | required, if `localhost`: add the port (e.g. `localhost:8080`) |
| PORT | int | port the API will use | required |
| JWT_SECRET | string | secret for the jwt auth | required |
| CORS_ALLOWED_ORIGINS | string | allowed origins for CORS | required, semicolon separated list |
| DB_FILE | string | path to the sqlite db file | required, can be at the root but name still required (e.g. `./RenewCMS.db`) ; have to end up with `.db` |
| DOCKER_DB_FOLDER | string | path to the sqlite db file's folder | required with docker, this will basically be the host machine folder (e.g. `./data`) that contains the sqlite db file |
| SMTP_EMAIL | string | sender email | required |
| SMTP_PASSWORD | string | smtp account password | required |
| SMTP_HOST | string | smtp server address | required |
| SMTP_PORT | int | smtp server port | required |
| Doc | Contents |
|---|---|
| [docs/development.md](docs/development.md) | Dev setup, how Vite and the API fit together, tests, lint, `task` commands |
| [docs/deployment.md](docs/deployment.md) | Building release binaries, running as a service, HTTPS, backups, upgrades |
| [docs/configuration.md](docs/configuration.md) | Every environment variable |
| [docs/architecture.md](docs/architecture.md) | Backend layering and how the SPA is embedded |
## API Usage
TODO
## Demo
TODO
## TODOs
- Cancel button when email validation pending or expired
+146
View File
@@ -0,0 +1,146 @@
version: '3'
vars:
BINARY: renewcms
DIST_DIR: internal/infrastructure/assets/dist
VERSION:
sh: git describe --tags --always --dirty
LDFLAGS: -s -w -X main.version={{.VERSION}}
RELEASE_TARGETS: [windows/amd64, windows/arm64, linux/amd64, linux/arm64, darwin/amd64, darwin/arm64]
env:
CGO_ENABLED: '0'
tasks:
default:
cmds:
- task --list
silent: true
setup:
desc: Install dev tools, Go modules and frontend dependencies
cmds:
- go install github.com/air-verse/air@latest
- go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
- go mod download
- task: web:install
dev:
desc: Run the API (air, hot reload) and the SPA (Vite, HMR) together → http://localhost:5173
deps: [dev:api, dev:web]
dev:api:
desc: Run the API with hot reload on :8080 (SPA not embedded)
cmds:
- air
dev:web:
desc: Run the Vite dev server on :5173, proxying /v1 to the API
dir: web
deps: [web:install]
cmds:
- bun dev
test:
desc: Run the Go tests
cmds:
- go test -tags nofrontend ./test/... -count=1 {{.CLI_ARGS}}
cover:
desc: Go test coverage of the use cases → tmp/coverage.out
cmds:
- mkdir -p tmp
- go test -tags nofrontend ./test/... -count=1 -coverpkg=./internal/infrastructure/useCases/... -coverprofile=tmp/coverage.out
- go tool cover -func=tmp/coverage.out
mutate:
desc: Gremlins mutation testing of the use cases → tmp/gremlins.json
cmds:
- mkdir -p tmp
- gremlins unleash --integration --tags nofrontend --coverpkg=./internal/infrastructure/useCases/...,./api/middleware/...,./api/pkg/claims/... --timeout-coefficient 10 --output tmp/gremlins.json ./internal/infrastructure/useCases ./api/middleware ./api/pkg/claims
test:web:
desc: Run the SPA test suite (Vitest) in run mode
dir: web
deps: [web:install]
cmds:
- bun run vitest run
lint:
desc: gofmt check, go vet, eslint (--fix) and vue-tsc
cmds:
- task: lint:go
- task: lint:web
lint:go:
cmds:
- cmd: test -z "$(gofmt -l .)" || (gofmt -l . && echo "unformatted Go files, run 'task fmt'" && exit 1)
silent: true
- go vet -tags nofrontend ./...
lint:web:
dir: web
deps: [web:install]
cmds:
- bun lint
- bun run type-check
fmt:
desc: Format Go and frontend sources
cmds:
- gofmt -w .
- bun run --cwd web format
web:install:
internal: true
dir: web
sources: [package.json, bun.lock]
generates: ['node_modules/.bin/vite*']
cmds:
- bun install --frozen-lockfile
build:web:
desc: Type-check and build the SPA into {{.DIST_DIR}}
dir: web
deps: [web:install]
sources: [src/**/*, public/**/*, index.html, vite.config.ts, tsconfig*.json, package.json, bun.lock]
generates: ['../{{.DIST_DIR}}/index.html']
cmds:
- bun run build
build:
desc: Production binary for this machine → bin/{{.BINARY}}
deps: [build:web]
cmds:
- go build -trimpath -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY}}{{exeExt}} ./cmd/server
release:
desc: Cross-compile production binaries for all release targets → bin/
deps: [build:web]
cmds:
- for: { var: RELEASE_TARGETS }
task: build:target
vars:
GOOS: '{{index (splitList "/" .ITEM) 0}}'
GOARCH: '{{index (splitList "/" .ITEM) 1}}'
build:target:
internal: true
vars:
EXT: '{{if eq .GOOS "windows"}}.exe{{end}}'
env:
GOOS: '{{.GOOS}}'
GOARCH: '{{.GOARCH}}'
cmds:
- go build -trimpath -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY}}_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}{{.EXT}} ./cmd/server
run:
desc: Build and run the production binary locally
deps: [build]
cmds:
- ./bin/{{.BINARY}}{{exeExt}}
clean:
desc: Remove build output, the built SPA and tmp/
cmds:
- rm -rf bin tmp {{.DIST_DIR}}
@@ -1,85 +0,0 @@
package gateways
import (
entity "RenewCMS/adapters/secondary/gateways/models"
"RenewCMS/domain/gateways"
domain "RenewCMS/domain/image"
"errors"
"github.com/google/uuid"
"gorm.io/gorm"
"mime/multipart"
"os"
)
type ImageRepository struct {
db *gorm.DB
}
func NewImageRepository(db *gorm.DB) *ImageRepository {
return &ImageRepository{db}
}
var contentTypeExtensions = map[string]string{
"image/png": ".png",
"image/jpeg": ".jpeg",
"image/webp": ".webp",
"image/svg+xml": ".svg",
}
func mapImageToDomain(image entity.Image) domain.Image {
return domain.FromDB(
image.ID,
image.Path,
image.ArticleID,
image.CreatedAt,
image.UpdatedAt,
)
}
func (i ImageRepository) Create(file multipart.File, fileHeader multipart.FileHeader) (domain.Image, error) {
fileBytes := make([]byte, fileHeader.Size)
_, err := file.Read(fileBytes)
if err != nil {
return domain.Image{}, err
}
err = os.MkdirAll("api/static/uploadedImages/", 0666)
if err != nil {
return domain.Image{}, err
}
contentType := fileHeader.Header.Get("Content-Type")
extension := contentTypeExtensions[contentType]
if extension == "" {
return domain.Image{}, errors.New("the file must be a PNG, JPEG, WEBP, or SVG image")
}
newName := uuid.NewString() + extension
err = os.WriteFile("api/static/uploadedImages/"+newName, fileBytes, 0666)
if err != nil {
return domain.Image{}, err
}
newImage := i.db.Create(&domain.Image{Path: "/static/uploadedImages/" + newName})
var createdImage entity.Image
newImage.Scan(&createdImage)
return mapImageToDomain(createdImage), nil
}
func (i ImageRepository) Delete(id uint32) error {
var image entity.Image
err := i.db.Model(&entity.Image{}).First(&image, id).Error
if err != nil {
return err
}
err = os.Remove("api" + image.Path)
if err != nil {
return err
}
return i.db.Delete(&entity.Image{}, id).Error
}
var _ gateways.IImageRepository = &ImageRepository{}
@@ -1,20 +0,0 @@
package models
import (
"gorm.io/gorm"
"time"
)
type User struct {
gorm.Model
ID uint32 `gorm:"primaryKey;autoIncrement"`
Username string `gorm:"unique;not null"`
Password string `gorm:"not null"`
PasswordResetCode string `gorm:"unique"`
Email string `gorm:"unique;not null"`
IsVerified bool `gorm:"default=false;not null"`
VerificationCode string `gorm:"unique"`
VerificationExpiration time.Time
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
@@ -1,32 +0,0 @@
package gateways
import (
"RenewCMS/domain/gateways"
"bytes"
"embed"
"html/template"
)
type PageRepository struct{}
//go:embed web/templates/*
var webTemplateFiles embed.FS
func NewPageRepository() *PageRepository {
return &PageRepository{}
}
func (p *PageRepository) Get(name string, data any) ([]byte, error) {
var processedHTML bytes.Buffer
tmpl, err := template.ParseFS(webTemplateFiles, "web/templates/"+name+".html")
if err != nil {
return nil, err
}
err = tmpl.Execute(&processedHTML, data)
if err != nil {
return nil, err
}
return processedHTML.Bytes(), nil
}
var _ gateways.IPageRepository = &PageRepository{}
@@ -1,152 +0,0 @@
package gateways
import (
entity "RenewCMS/adapters/secondary/gateways/models"
"RenewCMS/domain/gateways"
"RenewCMS/domain/user"
domain "RenewCMS/domain/user"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db}
}
func mapUserToDomain(user entity.User) domain.User {
return domain.FromDb(
user.ID,
user.Username,
user.Password,
user.PasswordResetCode,
user.Email,
user.IsVerified,
user.VerificationCode,
user.VerificationExpiration,
user.CreatedAt, user.UpdatedAt,
)
}
func (u *UserRepository) Get(id uint32) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, id).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
func (u *UserRepository) Create(user domain.User) (domain.User, error) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 12)
creationResult := u.db.Create(&entity.User{
Username: user.Username,
Password: string(hashedPassword),
Email: user.Email,
VerificationCode: string(hashedVerificationCode),
VerificationExpiration: user.VerificationExpiration,
})
if creationResult.Error != nil {
return domain.User{}, creationResult.Error
}
var createdUser entity.User
creationResult.Scan(&createdUser)
return mapUserToDomain(createdUser),
nil
}
func (u *UserRepository) Delete(id uint32) error {
return u.db.Delete(&user.User{}, id).Error
}
func (u *UserRepository) GetAll() []domain.User {
var users []entity.User
u.db.Model(&entity.User{}).Find(&users)
var domainUsers []domain.User
for _, localUser := range users {
domainUsers = append(domainUsers, mapUserToDomain(localUser))
}
return domainUsers
}
func (u *UserRepository) GetByUsername(username string) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
func (u *UserRepository) GetByEmail(email string) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).Where("email = ?", email).First(&localUser).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domain.User{}, err
}
localUser.IsVerified = isVerified
err = u.db.Save(&localUser).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
func (u *UserRepository) UpdatePassword(userId uint32, password string) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domain.User{}, err
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
localUser.Password = string(hashedPassword)
err = u.db.Save(&localUser).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (domain.User, error) {
var localUser entity.User
err := u.db.Model(&entity.User{}).First(&localUser, userId).Error
if err != nil {
return domain.User{}, err
}
hashedCode, _ := bcrypt.GenerateFromPassword([]byte(code), 12)
localUser.PasswordResetCode = string(hashedCode)
err = u.db.Save(&localUser).Error
if err != nil {
return domain.User{}, err
}
return mapUserToDomain(localUser), nil
}
var _ gateways.IUserRepository = &UserRepository{}
@@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Create article</title>
{{.Head}}
<style>
form {
max-width: 24rem;
}
</style>
</head>
<body class="text-dark">
{{.Navbar}}
<div class="container mt-3 text-black-50">
<h1>Article - creation</h1>
<p>Create a new article</p>
<form action="create" method="post" class="d-flex align-items-center flex-column gap-3" id="createArticleForm">
<div class="form-floating w-100">
<input type="text" class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" id="name"
name="name" placeholder="Chose the article name" value="{{ .Name }}">
<label for="name">Name</label>
<div class="invalid-feedback">{{ .PageError.Message }}</div>
</div>
<button id="createArticleButton" class="btn btn-primary w-100" disabled type="submit">
<span class="createArticleFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="createArticleFormButtonDefault">Create</span>
</button>
</form>
</div>
<script>
const button = document.querySelector("#createArticleButton")
const inputs = document.querySelectorAll('input')
const form = document.querySelector("#createArticleForm")
function formFieldsEmpty() {
return Array.from(inputs).some((input) => input.value === "")
}
function setButtonDisabled() {
button.disabled = formFieldsEmpty() ? "disabled" : ""
}
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector(".createArticleFormButtonDefault").classList.add("visually-hidden")
button.querySelector(".createArticleFormButtonLoading").classList.remove("visually-hidden")
}
function onCreateArticleFormSubmit(event) {
setButtonLoading()
event.target.submit()
}
function onInput(event) {
if (event.target.tagName === "INPUT") setButtonDisabled()
}
form.addEventListener('submit', onCreateArticleFormSubmit)
window.addEventListener('input', onInput)
setButtonDisabled()
</script>
</body>
</html>
@@ -1,94 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Article edition</title>
{{.Head}}
</head>
<body class="text-dark vh-100 d-flex flex-column">
{{.Navbar}}
<form class="container mt-3 text-black-50 d-flex flex-column h-100" action="edit" method="post">
<div class="d-flex align-items-center justify-content-between">
<div>
<h1>Article - edition</h1>
<label for="articleBody">Edition</label>
</div>
{{ if .Alert.Message }}
<div class="alert {{ if .Alert.IsError }} alert-danger {{ else }} alert-success {{ end }} alert-dismissible"
role="alert">
{{ .Alert.Message }}
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="alert"></button>
</div>
{{ end }}
<div class="d-flex justify-content-center align-items-center">
<button class="btn btn-success d-flex justify-content-center align-items-center gap-1" style="fill: white;"
type="submit">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z"/>
</svg>
Save
</button>
</div>
</div>
<div class="text-black-50 d-flex align-items-center justify-content-center h-100 py-3">
<textarea id="articleBody" name="articleBody">{{.Article.Body}}</textarea>
</div>
</form>
<script src="/static/tinymce/js/tinymce/tinymce.min.js"></script>
<script>
tinymce.init({
selector: '#articleBody',
promotion: false,
plugins: 'image lists visualblocks',
toolbar: 'undo redo | formatselect | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent | removeformat | image',
width: '100%',
height: '100%',
setup: function (editor) {
editor.on('init', function () {
console.log("LOADED")
});
},
license_key: 'gpl',
images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', "/article/{{.Article.ID}}/image/create");
xhr.upload.onprogress = (e) => {
progress(e.loaded / e.total * 100);
};
xhr.onload = () => {
if (xhr.status === 403) {
reject({message: 'HTTP Error: ' + xhr.status, remove: true});
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
reject('HTTP Error: ' + xhr.status);
return;
}
const json = JSON.parse(xhr.responseText);
if (!json || typeof json.location != 'string') {
reject('Invalid JSON: ' + xhr.responseText);
return;
}
resolve(`http{{ if .Secured }}s{{ end }}://${window.location.host}${json.location}`);
};
xhr.onerror = () => {
reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
};
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
xhr.send(formData);
}),
})
</script>
</body>
</html>
@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Articles</title>
{{.Head}}
<style>
.btn-outline-info:hover > *,
.btn-outline-danger:hover > * {
fill: #fff !important;
transition: fill ease-in-out 50ms;
}
</style>
</head>
<body class="text-dark">
{{.Navbar}}
<div class="container mt-3 text-black-50">
<h1>Articles</h1>
<p>List of your articles</p>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Creation Date</th>
<th scope="col">Edition Date</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5" class="text-center">
<form action="article/create" method="get">
<button class="btn btn-sm btn-link w-100 h-100" type="submit">Create a new article...</button>
</form>
</td>
</tr>
{{ range $article := .Articles }}
<tr>
<td>{{ $article.Title }}</td>
<td class="date">{{ $article.CreatedAt }}</td>
<td class="date">{{ $article.UpdatedAt }}</td>
<td>{{ if $article.IsOnline }}🟢 Online{{ else }}🟠 Offline{{ end }}</td>
<td>
<a href="/article/{{ $article.ID }}/edit" class="btn btn-outline-primary btn-sm">
<svg width="12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#pen"/>
</svg>
</a>
{{ if $article.IsOnline }}
<a href="/article/{{ $article.ID }}/unpublish" class="btn btn-outline-info btn-sm">
<svg width="12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#eye-slash"/>
</svg>
</a>
{{ else }}
<a href="/article/{{ $article.ID }}/publish" class="btn btn-outline-info btn-sm">
<svg width="12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#eye"/>
</svg>
</a>
{{ end }}
<a href="/article/{{ $article.Title }}/delete" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal"
data-bs-target="#{{ $article.ID }}">
<svg width=" 12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#trash"/>
</svg>
</a>
<div class="modal fade" id="{{ $article.ID }}" tabindex="-1" aria-labelledby="{{ $article.ID }}Label"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="{{ $article.ID }}Label">Delete post</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>You are about to delete the post <b>« {{ $article.Title }} »</b>, this is definitive,
are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel
</button>
<a href="/article/{{$article.ID}}/delete" class="btn btn-danger">Delete</a>
</div>
</div>
</div>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<script>
const datesElements = document.querySelectorAll("td.date")
datesElements.forEach(date => {
date.innerText = new Date(date.innerText).toLocaleDateString(undefined, {
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
})
</script>
</body>
</html>
@@ -1,32 +0,0 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<div class="d-flex gap-2 justify-content-center align-items-center">
<img src="/static/renewcms-favicon-64.png" alt="Logo" style="width: 2.5rem"/>
<a class="navbar-brand h1 my-auto" href="https://github.com/Floriansylvain/RenewCMS" target="_blank">
RenewCMS
</a>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/article">Articles</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/integration">Integration</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link disconnect-link" href="/logout">Log out</a>
</li>
</ul>
</div>
</div>
</nav>
@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Home</title>
{{.Head}}
</head>
<body class="text-dark">
{{.Navbar}}
<div class="container mt-3 text-black-50">
<h1>Home</h1>
<p>Welcome on RenewCMS !</p>
<a href="/article">
<button class="btn btn-outline-primary">Articles &raquo;</button>
</a>
</div>
</body>
</html>
@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Home</title>
{{.Head}}
</head>
<body class="text-dark">
{{.Navbar}}
<div class="container mt-3 text-black-50">
<h1>Implementation</h1>
<p>How to use headless API</p>
<div class="mt-5">
<section>
<h2>Introduction</h2>
<p>This documentation explains how to integrate our headless CMS API to retrieve article data via the <code>/article</code>
route.</p>
</section>
<section>
<h2>/v1/article</h2>
<p>The <code>{{ .Host }}/v1/article</code> route returns a list of articles in JSON format. Each article has the
following
structure:</p>
<pre>
<code>[
{
"id": 12,
"title": "This is a article!",
"body": "&lt;p&gt;?&lt;/p&gt;\r\n&lt;p&gt;&lt;img src=\"../../static/uploadedImages/649691a8-4f2e-48ca-9abe-27a60621d2e4.png\"
alt=\"\" width=\"391\" height=\"465\"&gt;&lt;/p&gt;",
"created_at": "2024-06-13T15:30:55.3563238+02:00",
"updated_at": "2024-06-13T15:31:17.5508931+02:00"
}
]</code>
</pre>
</section>
<section>
<h2>/v1/article/{id}</h2>
<p>The <code>{{ .Host }}/v1/article/{id}</code> route returns a single article in JSON format. The article has the
following
structure:</p>
<pre>
<code>{
"id": 12,
"title": "ravioli",
"body": "<\p>?<\/p>\r\n<\p><\img src=\"../../static/uploadedImages/649691a8-4f2e-48ca-9abe-27a60621d2e4.png\" alt=\"\"
width=\"391\" height=\"465\"><\/p>",
"images": [
{
"id": 17,
"path": "/static/uploadedImages/649691a8-4f2e-48ca-9abe-27a60621d2e4.png",
"article_id": 0,
"created_at": "2024-06-13T15:31:09.728359+02:00",
"updated_at": "2024-06-13T15:31:09.728359+02:00"
}
],
"created_at": "2024-06-13T15:30:55.3563238+02:00",
"updated_at": "2024-06-13T15:31:17.5508931+02:00"
}</code>
</pre>
</section>
</div>
</div>
</body>
</html>
@@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Login</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>Login</h2>
</div>
<form action="login" method="POST" class="mt-5" id="loginForm">
<div class="d-flex flex-column gap-4">
<div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}"
id="username"
name="username"
placeholder="Username"
required
type="text"
value="{{.Username}}">
<label for="username">Username</label>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}"
id="password"
name="password"
placeholder="Password"
required
type="password">
<label for="password">Password</label>
<div class="invalid-feedback">{{.PageError.Message}}</div>
</div>
<button id="loginFormButton" class="btn btn-primary" disabled type="submit">
<span class="loginFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="loginFormButtonDefault">Log in</span>
</button>
</div>
</form>
<div class="d-flex gap-2 my-2 w-100">
<a href="register" class="btn btn-outline-secondary w-100" type="submit">
First setup? Register
</a>
<a href="register/reset/request" class="btn btn-outline-secondary w-100" type="submit">
Forgot password
</a>
</div>
{{ if .Success }}
<div class="alert alert-success my-4" role="alert">
{{ .Success }}
</div>
{{ else if .Failure }}
<div class="alert alert-danger my-4" role="alert">
{{ .Failure }}
</div>
{{ end }}
</div>
</div>
<script>
const button = document.querySelector("#loginFormButton")
const inputs = document.querySelectorAll('input')
const form = document.querySelector("#loginForm")
function formFieldsEmpty() {
return Array.from(inputs).some((input) => input.value === "")
}
function isFormValid() {
return !formFieldsEmpty() && validateEmail()
}
function setButtonDisabled() {
button.disabled = isFormValid() ? "disabled" : ""
}
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector(".loginFormButtonDefault").classList.add("visually-hidden")
button.querySelector(".loginFormButtonLoading").classList.remove("visually-hidden")
}
function onLoginFormSubmit(event) {
event.preventDefault()
setButtonLoading()
event.target.submit()
}
function onInput(event) {
if (event.target.tagName === "INPUT") setButtonDisabled()
}
form.addEventListener('submit', onLoginFormSubmit)
window.addEventListener('input', onInput)
setButtonDisabled()
</script>
</body>
</html>
@@ -1,98 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Password reset</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>Admin account password reset</h2>
</div>
<form action="request" method="POST" class="mt-5" id="registerForm">
<div class="d-flex flex-column gap-4">
<div class="form-floating">
<input class="form-control {{ if .PageError.Email }} is-invalid {{ end }}"
id="email"
name="email"
placeholder="E-mail"
required
type="email"
value="{{.Email}}"
onblur="validateEmail()"
{{ if .Email }} disabled {{ end }}>
<label for="email">E-mail</label>
</div>
<button id="registerFormButton" class="btn btn-primary" disabled type="submit">
<span class="registerFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="registerFormButtonDefault">Send e-mail</span>
</button>
</div>
</form>
{{ if .Success }}
<div class="alert alert-success my-4" role="alert">
{{ .Success }}
</div>
{{ end }}
</div>
</div>
<script>
const docElems = {
form: document.querySelector("#registerForm"),
button: document.querySelector("#registerFormButton"),
inputs: document.querySelectorAll("input"),
email: document.querySelector("#email"),
}
function formSomeFieldsEmpty() {
return Array.from(docElems.inputs).some((input) => input.value === "")
}
function setButtonLoading() {
docElems.button.classList.add("disabled")
docElems.button.querySelector(".registerFormButtonDefault").classList.add("visually-hidden")
docElems.button.querySelector(".registerFormButtonLoading").classList.remove("visually-hidden")
}
function onRegisterFormSubmit(event) {
event.preventDefault()
setButtonLoading()
if (formSomeFieldsEmpty()) return
event.target.submit()
}
function validateEmail() {
const email = docElems.email.value.trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
docElems.email.classList.add("is-invalid")
return false
} else {
docElems.email.classList.remove("is-invalid")
return true
}
}
function onInput(event) {
if (event.target.tagName === "INPUT") {
docElems.button.disabled = !validateEmail() || formSomeFieldsEmpty()
}
}
docElems.form.addEventListener('submit', onRegisterFormSubmit)
window.addEventListener('input', onInput)
</script>
</body>
</html>
@@ -1,140 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Password reset</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>Admin account password reset</h2>
</div>
<form action="validate" method="POST" class="mt-5" id="registerForm">
<div class="d-flex flex-column gap-4">
<div class="form-floating">
<input class="form-control"
id="code"
name="code"
placeholder="Code"
type="text"
value="{{.Code}}"
readonly>
<label for="code">Code</label>
</div>
<div class="form-floating">
<input class="form-control"
id="email"
name="email"
placeholder="E-mail"
type="email"
value="{{.Email}}"
readonly>
<label for="email">E-mail</label>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="password"
name="password"
placeholder="Password"
required
type="password">
<label for="password">Password</label>
<div class="invalid-feedback">The password must contain at least 8 characters.</div>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="confirmPassword"
name="confirmPassword"
placeholder="Password confirmation"
required
type="password"
onblur="validatePasswordMatch()"
oninput="validatePasswordMatch()">
<label for="confirmPassword">Password confirmation</label>
<div class="invalid-feedback">The passwords do not match.</div>
</div>
<button id="registerFormButton" class="btn btn-primary" disabled type="submit">
<span class="registerFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="registerFormButtonDefault">Confirm changes</span>
</button>
</div>
</form>
{{ if .Error.Message }}
<div class="alert alert-danger my-4" role="alert">
{{ .Error.Message }}
</div>
{{ end }}
</div>
</div>
<script>
const docElems = {
form: document.querySelector("#registerForm"),
button: document.querySelector("#registerFormButton"),
inputs: document.querySelectorAll("input"),
email: document.querySelector("#email"),
password: document.querySelector("#password"),
confirmPassword: document.querySelector("#confirmPassword"),
}
function formSomeFieldsEmpty() {
return Array.from(docElems.inputs).some((input) => input.value === "")
}
function setButtonLoading() {
docElems.button.classList.add("disabled")
docElems.button.querySelector(".registerFormButtonDefault").classList.add("visually-hidden")
docElems.button.querySelector(".registerFormButtonLoading").classList.remove("visually-hidden")
}
function onRegisterFormSubmit(event) {
event.preventDefault()
setButtonLoading()
if (formSomeFieldsEmpty()) return
event.target.submit()
}
function validateEmail() {
const email = docElems.email.value.trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
docElems.email.classList.add("is-invalid")
return false
} else {
docElems.email.classList.remove("is-invalid")
return true
}
}
function onInput(event) {
if (event.target.tagName === "INPUT") {
docElems.button.disabled = !validateEmail() || formSomeFieldsEmpty()
}
}
function validatePasswordMatch() {
if (docElems.password.value !== docElems.confirmPassword.value) {
docElems.confirmPassword.classList.add("is-invalid")
return false
} else {
docElems.confirmPassword.classList.remove("is-invalid")
return true
}
}
docElems.form.addEventListener('submit', onRegisterFormSubmit)
window.addEventListener('input', onInput)
</script>
</body>
</html>
@@ -1,147 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Setup</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>Admin account creation</h2>
</div>
<form action="register" method="POST" class="mt-5" id="registerForm">
<div class="d-flex flex-column gap-4">
<div class="form-floating">
<input class="form-control {{ if .PageError.Email }} is-invalid {{ end }}"
id="email"
name="email"
placeholder="E-mail"
required
type="email"
value="{{.Email}}"
onblur="validateEmail()">
<label for="email">E-mail</label>
<div class="invalid-feedback">The e-mail must be a valid e-mail and not already in use.</div>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.Username }} is-invalid {{ end }}"
id="username"
name="username"
placeholder="Username"
required
type="text"
value="{{.Username}}">
<label for="username">Username</label>
<div class="invalid-feedback">The username must be between 3 and 20 characters long.</div>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="password"
name="password"
placeholder="Password"
required
type="password">
<label for="password">Password</label>
<div class="invalid-feedback">The password must contain at least 8 characters.</div>
</div>
<div class="form-floating">
<input class="form-control {{ if .PageError.Password }} is-invalid {{ end }}"
id="confirmPassword"
name="confirmPassword"
placeholder="Password confirmation"
required
type="password"
onblur="validatePasswordMatch()"
oninput="validatePasswordMatch()">
<label for="confirmPassword">Password confirmation</label>
<div class="invalid-feedback">The passwords do not match.</div>
</div>
<button id="registerFormButton" class="btn btn-primary" disabled type="submit">
<span class="registerFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="registerFormButtonDefault">Next</span>
</button>
</div>
</form>
<form action="login" method="get" class="mt-2 w-100">
<button class="btn btn-outline-primary w-100" type="submit">
Already registered? Log in!
</button>
</form>
</div>
</div>
<script>
const docElems = {
form: document.querySelector("#registerForm"),
button: document.querySelector("#registerFormButton"),
inputs: document.querySelectorAll("input"),
email: document.querySelector("#email"),
password: document.querySelector("#password"),
confirmPassword: document.querySelector("#confirmPassword"),
}
function formSomeFieldsEmpty() {
return Array.from(docElems.inputs).some((input) => input.value === "")
}
function isFormValid() {
return !formSomeFieldsEmpty() && validateEmail() && validatePasswordMatch()
}
function setButtonLoading() {
docElems.button.classList.add("disabled")
docElems.button.querySelector(".registerFormButtonDefault").classList.add("visually-hidden")
docElems.button.querySelector(".registerFormButtonLoading").classList.remove("visually-hidden")
}
function onRegisterFormSubmit(event) {
event.preventDefault()
setButtonLoading()
if (!isFormValid()) return
event.target.submit()
}
function onInput(event) {
if (event.target.tagName === "INPUT") {
docElems.button.disabled = !isFormValid()
}
}
function validateEmail() {
const email = docElems.email.value.trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
docElems.email.classList.add("is-invalid")
return false
} else {
docElems.email.classList.remove("is-invalid")
return true
}
}
function validatePasswordMatch() {
if (docElems.password.value !== docElems.confirmPassword.value) {
docElems.confirmPassword.classList.add("is-invalid")
return false
} else {
docElems.confirmPassword.classList.remove("is-invalid")
return true
}
}
docElems.form.addEventListener('submit', onRegisterFormSubmit)
window.addEventListener('input', onInput)
</script>
</body>
</html>
@@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Setup</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>Verify your email</h2>
</div>
<p>An e-mail with the validation link was sent to the address you just registered.</p>
<p>If you misspelled the address or just didn't receive any e-mails, please check your spams or cancel the
account creation.</p>
<form action="" method="post" class="d-flex">
<button class="btn btn-outline-primary w-100" type="submit">
<span class="visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span>Cancel</span>
</button>
</form>
</div>
</div>
<script>
const button = document.querySelector("button")
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector("button > span + span").classList.add("visually-hidden")
button.querySelector("button > span:first-child").classList.remove("visually-hidden")
}
window.addEventListener('submit', setButtonLoading)
button.addEventListener('click', setButtonLoading)
</script>
</body>
</html>
@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RenewCMS | Setup</title>
{{.Head}}
<style>
.form-container {
max-width: 24rem;
}
</style>
</head>
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
<div class="container">
<div class="d-flex flex-column gap-5 m-auto form-container">
<div>
<h1>RenewCMS</h1>
<h2>E-mail verification</h2>
</div>
{{ if .PageError.IsError }}
<p>{{.PageError.Message}}</p>
{{ else }}
<p>Your e-mail was successfully validated!</p>
<form action="../home" method="get" class="d-flex">
<button class="btn btn-primary w-100" type="submit">
<span class="visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span>Continue</span>
</button>
</form>
{{ end }}
</div>
</div>
<script>
const button = document.querySelector("button")
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector("button > span + span").classList.add("visually-hidden")
button.querySelector("button > span:first-child").classList.remove("visually-hidden")
}
window.addEventListener('submit', setButtonLoading)
button.addEventListener('click', setButtonLoading)
</script>
</body>
</html>
@@ -1,7 +0,0 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/bootstrap.min.css" type="text/css">
<script src="/static/bootstrap.min.js"
type="application/javascript"
defer></script>
-99
View File
@@ -1,99 +0,0 @@
package article
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"RenewCMS/domain/article"
"RenewCMS/useCases"
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type PostArticle struct {
Title string `json:"title" validate:"required,min=3,max=50"`
Body string `json:"body" validate:"required,max=10000"`
}
const IdUint32ErrorMessage = "The server expects the ID to be in the format of an unsigned 32-bit integer (uint32)."
func getArticle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, IdUint32ErrorMessage, http.StatusBadRequest)
return
}
localArticle, err := api.Container.GetArticleUseCase.GetArticle(uint32(id))
if err != nil || !localArticle.IsOnline {
http.Error(w, "The requested resource, identified by its unique ID, could not be found on the server.", http.StatusNotFound)
return
}
articleJson, _ := json.Marshal(localArticle)
_, _ = w.Write(articleJson)
}
func postArticle(w http.ResponseWriter, r *http.Request) {
var localArticle PostArticle
err := json.NewDecoder(r.Body).Decode(&localArticle)
if err != nil {
http.Error(w, auth.BodyErrorMessage, http.StatusBadRequest)
return
}
err = api.Validate.Struct(localArticle)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdArticle, err := api.Container.CreateArticleUseCase.CreateArticle(useCases.CreateArticleCommand{
Title: localArticle.Title,
Body: localArticle.Body,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleJson, _ := json.Marshal(createdArticle)
_, _ = w.Write(articleJson)
}
func listArticles(w http.ResponseWriter, _ *http.Request) {
articles := api.Container.ListArticlesUseCase.ListArticles()
onlineArticles := make([]article.Article, 0)
for _, localArticle := range articles {
if localArticle.IsOnline {
onlineArticles = append(onlineArticles, localArticle)
}
}
articlesJson, _ := json.Marshal(onlineArticles)
_, _ = w.Write(articlesJson)
}
func deleteArticle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, IdUint32ErrorMessage, http.StatusBadRequest)
}
err = api.Container.DeleteArticleUseCase.DeleteArticle(uint32(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
_, _ = w.Write([]byte("article deleted"))
}
func NewArticleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{id}", getArticle)
r.Post("/", postArticle)
r.Get("/", listArticles)
r.Delete("/{id}", deleteArticle)
return r
}
-190
View File
@@ -1,190 +0,0 @@
package auth
import (
"RenewCMS/api"
"RenewCMS/domain/user"
"RenewCMS/useCases"
"encoding/json"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"golang.org/x/crypto/bcrypt"
)
type RegisterCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
Email string `json:"email" validate:"required,email"`
}
type LoginCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
}
var shouldCookieBeSecure = os.Getenv("ENVIRONMENT") == "production"
var Token *jwtauth.JWTAuth
const LogsErrorMessage = "Access to the requested resource is forbidden due to incorrect password and/or username."
const BodyErrorMessage = "The request cannot be processed due to a mismatch in the format of the body."
func SetJwtCookie(w *http.ResponseWriter, userId uint32) error {
_, tokenString, err := Token.Encode(map[string]any{"user_id": userId})
if err != nil {
return err
}
http.SetCookie(*w, &http.Cookie{
Name: "jwt",
Value: tokenString,
Expires: time.Now().Add(24 * time.Hour),
Secure: shouldCookieBeSecure,
HttpOnly: true,
Path: "/",
})
return nil
}
func SomeUsersVerified() bool {
users := api.Container.ListUsersUseCase.ListUsers()
for _, localUser := range users {
if localUser.IsVerified {
return true
}
}
return false
}
func IsUserTableEmpty() bool {
users := api.Container.ListUsersUseCase.ListUsers()
return len(users) == 0
}
func IsLoggedIn(r *http.Request) bool {
token, err := jwtauth.VerifyRequest(
Token,
r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
return token != nil && err == nil
}
func IsVerified(r *http.Request) bool {
token, err := jwtauth.VerifyRequest(
Token,
r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
if err != nil {
return false
}
userId := token.PrivateClaims()["user_id"].(float64)
currentUser, _ := api.Container.GetUserUseCase.GetUser(uint32(userId))
return currentUser.IsVerified
}
func GetUserFromCredentials(credentials LoginCredentials) (user.User, error) {
dbUser, err := api.Container.GetUserUseCase.GetUserByUsername(credentials.Username)
if err != nil {
return user.User{}, err
}
err = bcrypt.CompareHashAndPassword([]byte(dbUser.Password), []byte(credentials.Password))
if err != nil {
return user.User{}, err
}
return dbUser, nil
}
func login(w http.ResponseWriter, r *http.Request) {
var credentials LoginCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, BodyErrorMessage, http.StatusBadRequest)
return
}
err = api.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
dbUser, err := GetUserFromCredentials(credentials)
if err != nil {
http.Error(w, LogsErrorMessage, http.StatusForbidden)
return
}
_ = SetJwtCookie(&w, dbUser.ID)
message, _ := json.Marshal(map[string]any{"message": "User logged in! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func register(w http.ResponseWriter, r *http.Request) {
if !IsLoggedIn(r) && !SomeUsersVerified() {
http.Error(w, "You are not allowed to create a user. Log in or reset database.", http.StatusForbidden)
return
}
var credentials RegisterCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, BodyErrorMessage, http.StatusBadRequest)
return
}
err = api.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdUser, err := api.Container.CreateUserUseCase.CreateUser(useCases.CreateUserCommand{
Username: credentials.Username,
Password: credentials.Password,
Email: credentials.Email,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = SetJwtCookie(&w, createdUser.ID)
message, _ := json.Marshal(map[string]any{"message": "User registered! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func RemoveJwtCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: "",
Expires: time.Now(),
MaxAge: -1,
Secure: shouldCookieBeSecure,
HttpOnly: true,
Path: "/",
})
}
func logout(w http.ResponseWriter, _ *http.Request) {
RemoveJwtCookie(w)
message, _ := json.Marshal(map[string]any{"message": "User logged out! HTTPonly jwt cookie deleted"})
_, _ = w.Write(message)
}
func NewAuthRouter() http.Handler {
r := chi.NewRouter()
r.Post("/register", register)
r.Post("/login", login)
r.Post("/logout", logout)
return r
}
-40
View File
@@ -1,40 +0,0 @@
package image
import (
"RenewCMS/api"
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
func PostImage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
id := chi.URLParam(r, "id")
idInt, _ := strconv.Atoi(id)
file, fileHeader, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
newImage, err := api.Container.CreateImageUseCase.CreateImage(file, *fileHeader)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = api.Container.UpdateArticleUseCase.AddImage(uint32(idInt), newImage.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
newJson := map[string]any{"location": newImage.Path}
newJsonBytes, _ := json.Marshal(newJson)
_, _ = w.Write(newJsonBytes)
}
-58
View File
@@ -1,58 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/useCases"
"html/template"
"net/http"
"regexp"
"strconv"
)
type ArticleCreatePageError struct {
IsError bool
Message string
}
func GetArticleCreatePageTemplate(postName string, errorMessage string) ([]byte, error) {
navbarTmpl, _ := api.Container.GetPageUseCase.GetPage("componentNavbar", nil)
return api.Container.GetPageUseCase.GetPage("articleCreate", map[string]any{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
"PageError": ArticleCreatePageError{
IsError: errorMessage != "",
Message: errorMessage,
},
"Name": postName,
})
}
func PostArticleCreatePage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
postName := r.FormValue("name")
pattern := regexp.MustCompile("^[a-zA-Z0-9À-ÖØ-öø-ÿĀ-ſḀ-ỿ ]{3,50}$")
if !pattern.MatchString(postName) {
postsTmpl, _ := GetArticleCreatePageTemplate(postName, "Name should be alphanumeric, and between 3 and 50 characters.")
_, _ = w.Write(postsTmpl)
return
}
post, err := api.Container.CreateArticleUseCase.CreateArticle(useCases.CreateArticleCommand{
Title: postName,
Body: "",
})
if err != nil {
postsTmpl, _ := GetArticleCreatePageTemplate(postName, "Something went wrong when creating the article, please contact admin.")
_, _ = w.Write(postsTmpl)
return
}
http.Redirect(w, r, "/article/"+strconv.Itoa(int(post.ID))+"/edit", http.StatusSeeOther)
}
func GetArticleCreatePage(w http.ResponseWriter, _ *http.Request) {
postsTmpl, _ := GetArticleCreatePageTemplate("", "")
_, _ = w.Write(postsTmpl)
}
-42
View File
@@ -1,42 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/article"
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
func GetArticleDeletePage(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, article.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
localArticle, err := api.Container.GetArticleUseCase.GetArticle(uint32(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
fmt.Println(localArticle.Images)
for _, image := range localArticle.Images {
err = api.Container.DeleteImageUseCase.DeleteImage(image.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
}
err = api.Container.DeleteArticleUseCase.DeleteArticle(uint32(id))
if err != nil {
http.Error(w, http.StatusText(400), http.StatusBadRequest)
return
}
http.Redirect(w, r, "/article", http.StatusSeeOther)
}
-75
View File
@@ -1,75 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/domain/article"
"html/template"
"net/http"
"os"
"strconv"
"github.com/go-chi/chi/v5"
)
type ArticleEditPageAlert struct {
IsError bool
Message string
}
func getArticleEditPageTemplate(post article.Article, alert ArticleEditPageAlert) []byte {
navbarTmpl, _ := api.Container.GetPageUseCase.GetPage("componentNavbar", nil)
articleTmpl, _ := api.Container.GetPageUseCase.GetPage("articleEdit", map[string]any{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
"Article": post,
"Alert": alert,
"Secured": os.Getenv("ENVIRONMENT") == "production",
})
return articleTmpl
}
func PostArticleEditPage(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "id")
articleIDint, err := strconv.Atoi(articleID)
if err != nil {
_, _ = w.Write(getArticleEditPageTemplate(article.Article{}, ArticleEditPageAlert{
IsError: true,
Message: "Could not find the requested article.",
}))
return
}
_ = r.ParseForm()
articleBody := r.FormValue("articleBody")
getArticle, _ := api.Container.GetArticleUseCase.GetArticle(uint32(articleIDint))
updatedArticle, err := api.Container.UpdateArticleUseCase.UpdateBody(getArticle.ID, articleBody)
if err != nil {
_, _ = w.Write(getArticleEditPageTemplate(article.Article{Body: articleBody}, ArticleEditPageAlert{
IsError: true,
Message: "Could not save the article: " + err.Error(),
}))
return
}
_, _ = w.Write(getArticleEditPageTemplate(updatedArticle, ArticleEditPageAlert{
IsError: false,
Message: "Article successfully edited!",
}))
}
func GetArticleEditPage(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "id")
articleIDint, err := strconv.Atoi(articleID)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
getArticle, _ := api.Container.GetArticleUseCase.GetArticle(uint32(articleIDint))
_, _ = w.Write(getArticleEditPageTemplate(getArticle, ArticleEditPageAlert{
IsError: false,
Message: "",
}))
}
@@ -1,44 +0,0 @@
package pages
import (
"RenewCMS/api"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
func updateIsOnline(articleId string, isOnline bool) (error, int) {
articleIdInt, err := strconv.Atoi(articleId)
if err != nil {
return errors.New("the server expects the ID to be in the format of an unsigned 32-bit integer (uint32)"), http.StatusBadRequest
}
_, err = api.Container.UpdateArticleUseCase.UpdateIsOnline(uint32(articleIdInt), isOnline)
if err != nil {
return errors.New("the requested resource, identified by its unique ID, could not be found on the server"), http.StatusNotFound
}
return nil, http.StatusOK
}
func GetArticleUnpublishPage(w http.ResponseWriter, r *http.Request) {
articleId := chi.URLParam(r, "id")
err, statusCode := updateIsOnline(articleId, false)
if err != nil {
http.Error(w, err.Error(), statusCode)
return
}
http.Redirect(w, r, "/article", http.StatusSeeOther)
}
func GetArticlePublishPage(w http.ResponseWriter, r *http.Request) {
articleId := chi.URLParam(r, "id")
err, statusCode := updateIsOnline(articleId, true)
if err != nil {
http.Error(w, err.Error(), statusCode)
return
}
http.Redirect(w, r, "/article", http.StatusSeeOther)
}
-31
View File
@@ -1,31 +0,0 @@
package pages
import (
"RenewCMS/api"
"html/template"
"net/http"
)
func GetArticlesPage(w http.ResponseWriter, _ *http.Request) {
articles := api.Container.ListArticlesUseCase.ListArticles()
var formattedArticles []map[string]any
for _, article := range articles {
formattedArticles = append(formattedArticles, map[string]any{
"ID": article.ID,
"Title": article.Title,
"IsOnline": article.IsOnline,
"CreatedAt": article.CreatedAt.Format("2006-01-02 15:04:05"),
"UpdatedAt": article.UpdatedAt.Format("2006-01-02 15:04:05"),
})
}
navbarTmpl, _ := api.Container.GetPageUseCase.GetPage("componentNavbar", nil)
articlesTmpl, _ := api.Container.GetPageUseCase.GetPage("articles", map[string]any{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
"Articles": formattedArticles,
})
_, _ = w.Write(articlesTmpl)
}
-16
View File
@@ -1,16 +0,0 @@
package pages
import (
"RenewCMS/api"
"html/template"
"net/http"
)
func GetHomePage(w http.ResponseWriter, _ *http.Request) {
navbarTmpl, _ := api.Container.GetPageUseCase.GetPage("componentNavbar", nil)
homeTmpl, _ := api.Container.GetPageUseCase.GetPage("home", map[string]any{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
})
_, _ = w.Write(homeTmpl)
}
-18
View File
@@ -1,18 +0,0 @@
package pages
import (
"RenewCMS/api"
"html/template"
"net/http"
"os"
)
func GetPageIntegration(w http.ResponseWriter, _ *http.Request) {
navbarTmpl, _ := api.Container.GetPageUseCase.GetPage("componentNavbar", nil)
templ, _ := api.Container.GetPageUseCase.GetPage("integration", map[string]any{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
"Host": os.Getenv("HOST"),
})
_, _ = w.Write(templ)
}
-79
View File
@@ -1,79 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"net/http"
"net/url"
)
const LoginRoute = "/login"
type LoginPage struct {
PageError *PageError `json:"error"`
Username string `json:"username"`
}
var EmptyLoginPage = &LoginPage{
PageError: NewPageError(""),
Username: "",
}
func GetLoginPageHandler(loginPage *LoginPage) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if auth.IsLoggedIn(r) {
http.Redirect(w, r, "/home", http.StatusSeeOther)
return
}
if r.Method == http.MethodPost {
PostLoginPage(w, r)
return
}
if auth.IsUserTableEmpty() {
http.Redirect(w, r, "/register", http.StatusSeeOther)
return
}
success, _ := url.QueryUnescape(r.URL.Query().Get("success"))
failure, _ := url.QueryUnescape(r.URL.Query().Get("failure"))
bs, _ := api.Container.GetPageUseCase.GetPage("login", map[string]any{
"PageError": loginPage.PageError,
"Username": loginPage.Username,
"Head": headTmpl,
"Success": success,
"Failure": failure,
})
_, _ = w.Write(bs)
}
}
func PostLoginPage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
credentials := auth.LoginCredentials{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
}
err := api.Validate.Struct(credentials)
if err != nil {
r.Method = http.MethodGet
GetLoginPageHandler(&LoginPage{
PageError: NewPageError("Invalid username or password format."),
Username: r.FormValue("username"),
})(w, r)
return
}
dbUser, err := auth.GetUserFromCredentials(credentials)
if err != nil {
r.Method = http.MethodGet
GetLoginPageHandler(&LoginPage{
PageError: NewPageError("Invalid username or password combination."),
Username: r.FormValue("username"),
})(w, r)
return
}
_ = auth.SetJwtCookie(&w, dbUser.ID)
http.Redirect(w, r, "/home", http.StatusSeeOther)
}
-162
View File
@@ -1,162 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"RenewCMS/api/controllers/image"
"html/template"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
)
var headTmpl template.HTML
var contentTypes = map[string]string{
".css": "text/css",
".js": "application/javascript",
".png": "image/png",
".jpg": "image/jpeg",
".webp": "image/webp",
".svg": "image/svg+xml",
".ico": "image/x-icon",
}
type PageError struct {
Message string `json:"message"`
IsError bool `json:"isError"`
}
func NewPageError(message string) *PageError {
return &PageError{
Message: message,
IsError: strings.Compare(message, "") != 0,
}
}
func IsLoggedInMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !auth.IsLoggedIn(r) {
http.Redirect(w, r, LoginRoute, http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func IsVerifiedMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !auth.IsVerified(r) {
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func IsNotVerifiedMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if auth.IsVerified(r) {
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func GetLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, LoginRoute, http.StatusPermanentRedirect)
}
func GetLogout(w http.ResponseWriter, r *http.Request) {
auth.RemoveJwtCookie(w)
http.Redirect(w, r, LoginRoute, http.StatusSeeOther)
}
func StaticFileServerWithContentType(fsys http.FileSystem) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
filePath := filepath.Join("api/static", r.URL.Path)
fileInfo, err := os.Stat(filePath)
if err == nil && fileInfo.IsDir() {
http.Error(w, "Folder direct access is disabled.", http.StatusForbidden)
return
}
if ext := filepath.Ext(path); ext != "" {
if ct, ok := contentTypes[ext]; ok {
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("Content-Type", ct)
}
}
http.FileServer(fsys).ServeHTTP(w, r)
})
}
func InitHeadTmpl() {
headTmplHtml, _ := api.Container.GetPageUseCase.GetPage("utilsHead", nil)
headTmpl = template.HTML(headTmplHtml)
}
func NewPageRouter() http.Handler {
r := chi.NewRouter()
InitHeadTmpl()
fileServer := StaticFileServerWithContentType(http.Dir("api/static"))
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
r.Get("/", GetLogin)
r.Get(LoginRoute, GetLoginPageHandler(EmptyLoginPage))
r.Post(LoginRoute, GetLoginPageHandler(EmptyLoginPage))
r.Get("/register", GetRegisterPageHandler(EmptyRegisterPage))
r.Post("/register", GetRegisterPageHandler(EmptyRegisterPage))
r.Get("/logout", GetLogout)
r.Get("/register/reset/request", GetPasswordResetRequest)
r.Post("/register/reset/request", PostPasswordResetRequest)
r.Get("/register/reset/validate", GetPasswordResetValidate)
r.Post("/register/reset/validate", PostPasswordResetValidate)
r.Group(func(r chi.Router) {
r.Use(IsLoggedInMiddleware)
r.Use(IsNotVerifiedMiddleware)
r.Get("/register/pending", GetRegisterPendingPage)
r.Post("/register/pending", PostRegisterPendingPage)
r.Get("/register/validate", GetRegisterValidatePage)
})
r.Group(func(r chi.Router) {
r.Use(IsLoggedInMiddleware)
r.Use(IsVerifiedMiddleware)
r.Get("/home", GetHomePage)
r.Get("/article", GetArticlesPage)
r.Get("/article/{id}/edit", GetArticleEditPage)
r.Post("/article/{id}/edit", PostArticleEditPage)
r.Get("/article/{id}/delete", GetArticleDeletePage)
r.Get("/article/create", GetArticleCreatePage)
r.Post("/article/create", PostArticleCreatePage)
r.Post("/article/{id}/image/create", image.PostImage)
r.Get("/article/{id}/publish", GetArticlePublishPage)
r.Get("/article/{id}/unpublish", GetArticleUnpublishPage)
r.Get("/integration", GetPageIntegration)
})
return r
}
@@ -1,58 +0,0 @@
package pages
import (
"RenewCMS/api"
"net/http"
"os"
"github.com/google/uuid"
)
type PasswordResetRequest struct {
Email string `json:"email"`
}
var successMessage = "An email has been sent to the provided email address if it exists."
func GetPasswordResetRequest(w http.ResponseWriter, r *http.Request) {
success := r.URL.Query().Get("success")
email := r.URL.Query().Get("email")
bs, _ := api.Container.GetPageUseCase.GetPage("passwordResetRequest", map[string]any{
"Head": headTmpl,
"Email": email,
"Success": success,
})
_, _ = w.Write(bs)
}
func PostPasswordResetRequest(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
passResetReq := PasswordResetRequest{
Email: r.FormValue("email"),
}
var getRedirectUrl = "/register/reset/request?success=" + successMessage + "&email=" + passResetReq.Email
err := api.Validate.Struct(passResetReq)
if err != nil {
http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
return
}
fetchedUser, err := api.Container.GetUserUseCase.GetUserByEmail(passResetReq.Email)
if err != nil {
http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
return
}
verificationCode := uuid.NewString()
updatedUser, _ := api.Container.UpdateUserUseCase.UpdatePasswordResetCode(fetchedUser.ID, verificationCode)
_ = api.Container.SendMailUseCase.SendMail(updatedUser.Email, "passwordReset", map[string]string{
"Host": os.Getenv("HOST"),
"VerificationCode": verificationCode,
"Email": updatedUser.Email,
})
http.Redirect(w, r, getRedirectUrl, http.StatusSeeOther)
}
@@ -1,57 +0,0 @@
package pages
import (
"RenewCMS/api"
"net/http"
"golang.org/x/crypto/bcrypt"
)
var PasswordLinkErrorMessage = "The reset password link you used is invalid."
var PasswordLinkSuccessMessage = "Your account password was successfully updated."
func GetPasswordResetValidate(w http.ResponseWriter, r *http.Request) {
failure := r.URL.Query().Get("failure")
pageError := NewPageError("")
if failure != "" {
pageError = NewPageError(failure)
}
template, _ := api.Container.GetPageUseCase.GetPage("passwordResetValidate", map[string]any{
"Head": headTmpl,
"Error": pageError,
"Email": r.URL.Query().Get("email"),
"Code": r.URL.Query().Get("c"),
})
_, _ = w.Write(template)
}
func PostPasswordResetValidate(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
email := r.FormValue("email")
password := r.FormValue("password")
resetCode := r.FormValue("code")
var redirectionErrorLink = "/register/reset/validate?email=" + email + "&c=" + resetCode
if len(password) < 8 {
http.Redirect(w, r, redirectionErrorLink+"&failure=Password length should be at least 8 characters.", http.StatusSeeOther)
return
}
fetchedUser, err := api.Container.GetUserUseCase.GetUserByEmail(email)
if err != nil {
http.Redirect(w, r, redirectionErrorLink+"&failure="+PasswordLinkErrorMessage, http.StatusSeeOther)
return
}
err = bcrypt.CompareHashAndPassword([]byte(fetchedUser.PasswordResetCode), []byte(resetCode))
if err != nil {
http.Redirect(w, r, redirectionErrorLink+"&failure="+PasswordLinkErrorMessage, http.StatusSeeOther)
return
}
_, _ = api.Container.UpdateUserUseCase.UpdatePassword(fetchedUser.ID, password)
_, _ = api.Container.UpdateUserUseCase.UpdatePasswordResetCode(fetchedUser.ID, "")
http.Redirect(w, r, "/login?success="+PasswordLinkSuccessMessage, http.StatusSeeOther)
}
-109
View File
@@ -1,109 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"RenewCMS/useCases"
"net/http"
"os"
)
type RegisterPageError struct {
Email bool `json:"email"`
Password bool `json:"password"`
Username bool `json:"username"`
}
type RegisterPage struct {
PageError *RegisterPageError `json:"error"`
Username string `json:"username"`
Email string `json:"email"`
}
var EmptyRegisterPage = &RegisterPage{
PageError: &RegisterPageError{
Email: false,
Password: false,
Username: false,
},
Username: "",
Email: "",
}
func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !auth.IsLoggedIn(r) && auth.SomeUsersVerified() {
http.Redirect(w, r, "/login?failure=There is already a verified account, please login.", http.StatusSeeOther)
}
if (auth.IsLoggedIn(r) && auth.IsVerified(r)) || auth.SomeUsersVerified() {
http.Redirect(w, r, "/home", http.StatusSeeOther)
return
}
if r.Method == http.MethodPost {
PostRegisterPage(w, r)
return
}
bs, err := api.Container.GetPageUseCase.GetPage("register", map[string]any{
"PageError": registerPage.PageError,
"Username": registerPage.Username,
"Email": registerPage.Email,
"Head": headTmpl,
})
if err != nil {
_, _ = w.Write([]byte(err.Error()))
}
_, _ = w.Write(bs)
}
}
func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
credentials := auth.RegisterCredentials{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
Email: r.FormValue("email"),
}
err := api.Validate.Struct(credentials)
if err != nil {
r.Method = http.MethodGet
GetRegisterPageHandler(&RegisterPage{
PageError: &RegisterPageError{
Email: true,
Password: true,
Username: true,
},
Username: r.FormValue("username"),
Email: r.FormValue("email"),
})(w, r)
return
}
createdUser, err := api.Container.CreateUserUseCase.CreateUser(useCases.CreateUserCommand{
Username: credentials.Username,
Password: credentials.Password,
Email: credentials.Email,
})
if err != nil {
r.Method = http.MethodGet
GetRegisterPageHandler(&RegisterPage{
PageError: &RegisterPageError{
Email: true,
Password: false,
Username: false,
},
Username: r.FormValue("username"),
Email: r.FormValue("email"),
})(w, r)
return
}
_ = api.Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{
"Host": os.Getenv("HOST"),
"VerificationCode": createdUser.VerificationCode,
})
_ = auth.SetJwtCookie(&w, createdUser.ID)
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
}
-36
View File
@@ -1,36 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"log"
"net/http"
"github.com/go-chi/jwtauth/v5"
)
func PostRegisterPendingPage(w http.ResponseWriter, r *http.Request) {
token, _ := jwtauth.VerifyRequest(
auth.Token, r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
userId := token.PrivateClaims()["user_id"].(float64)
err := api.Container.DeleteUserUseCase.DeleteUser(uint32(userId))
if err != nil {
log.Println(err)
r.Method = http.MethodGet
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
}
auth.RemoveJwtCookie(w)
http.Redirect(w, r, "/register", http.StatusSeeOther)
}
func GetRegisterPendingPage(w http.ResponseWriter, _ *http.Request) {
registerPendingTmpl, _ := api.Container.GetPageUseCase.GetPage("registerPending", map[string]any{
"Head": headTmpl,
})
_, _ = w.Write(registerPendingTmpl)
}
-46
View File
@@ -1,46 +0,0 @@
package pages
import (
"RenewCMS/api"
"RenewCMS/api/controllers/auth"
"net/http"
"time"
"github.com/go-chi/jwtauth/v5"
"golang.org/x/crypto/bcrypt"
)
func GetRegisterValidatePage(w http.ResponseWriter, r *http.Request) {
queryVerificationCode := r.URL.Query().Get("c")
if queryVerificationCode == "" {
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
return
}
token, _ := jwtauth.VerifyRequest(
auth.Token,
r,
jwtauth.TokenFromCookie,
jwtauth.TokenFromHeader,
jwtauth.TokenFromQuery)
userId := token.PrivateClaims()["user_id"].(float64)
user, _ := api.Container.GetUserUseCase.GetUser(uint32(userId))
errorMessage := ""
err := bcrypt.CompareHashAndPassword([]byte(user.VerificationCode), []byte(queryVerificationCode))
if err != nil || user.VerificationExpiration.Before(time.Now()) {
errorMessage = "Verification link is incorrect or has expired."
} else {
_, err := api.Container.UpdateUserUseCase.UpdateVerificationStatus(user.ID, true)
if err != nil {
errorMessage = "Something went wrong server-side. User account may not exist."
}
}
registerValidateTmpl, _ := api.Container.GetPageUseCase.GetPage("registerValidate", map[string]any{
"Head": headTmpl,
"PageError": NewPageError(errorMessage),
})
_, _ = w.Write(registerValidateTmpl)
}
+72 -52
View File
@@ -1,46 +1,52 @@
package api
import (
"RenewCMS/adapters/secondary/gateways"
"RenewCMS/adapters/secondary/gateways/models"
domainGateways "RenewCMS/domain/gateways"
"RenewCMS/useCases"
domainArticle "RenewCMS/internal/domain/article"
domainImage "RenewCMS/internal/domain/image"
domainMail "RenewCMS/internal/domain/mail"
domainUser "RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/config"
"RenewCMS/internal/infrastructure/hashing"
"RenewCMS/internal/infrastructure/mailer"
"RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"RenewCMS/internal/infrastructure/useCases"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"github.com/go-chi/jwtauth/v5"
"gorm.io/gorm"
)
type UseCases struct {
CreateArticleUseCase *useCases.CreateArticleUseCase
GetArticleUseCase *useCases.GetArticleUseCase
ListArticlesUseCase *useCases.ListArticlesUseCase
UpdateArticleUseCase *useCases.UpdateArticleUseCase
DeleteArticleUseCase *useCases.DeleteArticleUseCase
GetUserUseCase *useCases.GetUserUseCase
CreateUserUseCase *useCases.CreateUserUseCase
UpdateUserUseCase *useCases.UpdateUserUseCase
DeleteUserUseCase *useCases.DeleteUserUseCase
ListUsersUseCase *useCases.ListUsersUseCase
GetPageUseCase *useCases.GetPageUseCase
SendMailUseCase *useCases.SendMailUseCase
CreateImageUseCase *useCases.CreateImageUseCase
DeleteImageUseCase *useCases.DeleteImageUseCase
Token *jwtauth.JWTAuth
CreateArticleUseCase *useCases.CreateArticleUseCase
GetArticleUseCase *useCases.GetArticleUseCase
ListArticlesUseCase *useCases.ListArticlesUseCase
UpdateArticleUseCase *useCases.UpdateArticleUseCase
DeleteArticleUseCase *useCases.DeleteArticleUseCase
GetUserUseCase *useCases.GetUserUseCase
CreateUserUseCase *useCases.CreateUserUseCase
AuthenticateUserUseCase *useCases.AuthenticateUserUseCase
VerifyUserUseCase *useCases.VerifyUserUseCase
CancelRegistrationUseCase *useCases.CancelRegistrationUseCase
RequestPasswordResetUseCase *useCases.RequestPasswordResetUseCase
ResetPasswordUseCase *useCases.ResetPasswordUseCase
ListUsersUseCase *useCases.ListUsersUseCase
SendMailUseCase *useCases.SendMailUseCase
CreateImageUseCase *useCases.CreateImageUseCase
DeleteImageUseCase *useCases.DeleteImageUseCase
}
type Repositories struct {
ArticleRepo domainGateways.IArticleRepository
UserRepo domainGateways.IUserRepository
ImageRepo domainGateways.IImageRepository
MailRepo domainGateways.IMailRepository
PageRepo domainGateways.IPageRepository
ArticleRepo domainArticle.Repository
UserRepo domainUser.Repository
ImageRepo domainImage.Repository
MailRepo domainMail.Repository
}
var Container *UseCases
func getDb() *gorm.DB {
dbName := os.Getenv("DB_FILE")
func getDb(dbName string) *gorm.DB {
if err := os.MkdirAll(filepath.Dir(dbName), os.ModePerm); err != nil {
panic("Unable to create necessary subdirectories: " + err.Error())
}
@@ -56,37 +62,51 @@ func getDb() *gorm.DB {
return db
}
func getRepositories(db *gorm.DB) *Repositories {
func getRepositories(db *gorm.DB, settings config.Settings) *Repositories {
return &Repositories{
ArticleRepo: gateways.NewArticleRepository(db),
UserRepo: gateways.NewUserRepository(db),
ImageRepo: gateways.NewImageRepository(db),
MailRepo: gateways.NewMailRepository(),
PageRepo: gateways.NewPageRepository(),
ArticleRepo: persistence.NewArticleRepository(db),
UserRepo: persistence.NewUserRepository(db),
ImageRepo: persistence.NewImageRepository(db, settings.UploadDir),
MailRepo: mailer.NewMailRepository(settings.Smtp),
}
}
func getUseCases(repos *Repositories) *UseCases {
func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
hasher := hashing.NewBcryptHasher()
return &UseCases{
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo),
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo),
GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo),
CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo),
UpdateUserUseCase: useCases.NewUpdateUserUseCase(repos.UserRepo),
DeleteUserUseCase: useCases.NewDeleteUserUseCase(repos.UserRepo),
ListUsersUseCase: useCases.NewListUsersUseCase(repos.UserRepo),
GetPageUseCase: useCases.NewGetPageUseCase(repos.PageRepo),
SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo),
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo),
DeleteImageUseCase: useCases.NewDeleteImageUseCase(repos.ImageRepo),
Token: jwtauth.New("HS256", []byte(settings.JWTSecret), nil),
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo),
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo, repos.ImageRepo),
GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo),
CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL),
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(repos.UserRepo, hasher),
VerifyUserUseCase: useCases.NewVerifyUserUseCase(repos.UserRepo, hasher),
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(repos.UserRepo),
RequestPasswordResetUseCase: useCases.NewRequestPasswordResetUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL),
ResetPasswordUseCase: useCases.NewResetPasswordUseCase(repos.UserRepo, hasher),
ListUsersUseCase: useCases.NewListUsersUseCase(repos.UserRepo),
SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo),
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo),
DeleteImageUseCase: useCases.NewDeleteImageUseCase(repos.ImageRepo),
}
}
func InitContainer() {
db := getDb()
repos := getRepositories(db)
Container = getUseCases(repos)
func InitContainer(settings config.Settings) (*UseCases, func() error) {
db := getDb(settings.DBFile)
repos := getRepositories(db, settings)
return getUseCases(repos, settings), closeDb(db)
}
func closeDb(db *gorm.DB) func() error {
return func() error {
sqlDb, err := db.DB()
if err != nil {
return err
}
return sqlDb.Close()
}
}
+249
View File
@@ -0,0 +1,249 @@
package article
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/api/pkg/claims"
validator "RenewCMS/api/pkg/validator"
domain "RenewCMS/internal/domain/article"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type Handler struct {
CreateUseCase *useCases.CreateArticleUseCase
GetUseCase *useCases.GetArticleUseCase
ListUseCase *useCases.ListArticlesUseCase
DeleteUseCase *useCases.DeleteArticleUseCase
UpdateUseCase *useCases.UpdateArticleUseCase
GetUserUseCase *useCases.GetUserUseCase
}
type PostArticle struct {
Title string `json:"title" validate:"required,min=3,max=200"`
Body string `json:"body" validate:"required,max=10000"`
}
func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleUseCase, list *useCases.ListArticlesUseCase, delete *useCases.DeleteArticleUseCase, update *useCases.UpdateArticleUseCase, getUser *useCases.GetUserUseCase) *Handler {
return &Handler{
CreateUseCase: create,
GetUseCase: get,
ListUseCase: list,
DeleteUseCase: delete,
UpdateUseCase: update,
GetUserUseCase: getUser,
}
}
func parseArticleId(r *http.Request) (uint32, error) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
return uint32(id), err
}
func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
var article domain.Article
if h.callerIsVerified(r) {
article, err = h.GetUseCase.GetArticle(id)
} else {
article, err = h.GetUseCase.GetOnlineArticle(id)
}
if err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
articleJson, err := json.Marshal(article)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articleJson); err != nil {
fmt.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
var localArticle PostArticle
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(localArticle); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdArticle, err := h.CreateUseCase.CreateArticle(useCases.CreateArticleCommand{
Title: localArticle.Title,
Body: localArticle.Body,
})
if err != nil {
if errors.Is(err, domain.ErrInvalidTitle) {
http.Error(w, apperr.ArticleTitleErrorMessage, http.StatusBadRequest)
return
}
http.Error(w, "Failed to create article", http.StatusInternalServerError)
return
}
articleJson, err := json.Marshal(createdArticle)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func writeArticleErrorResponse(w http.ResponseWriter, err error) {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
func writeArticleJsonResponse(w http.ResponseWriter, article domain.Article) {
articleJson, err := json.Marshal(article)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) setIsOnline(w http.ResponseWriter, r *http.Request, isOnline bool) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
updatedArticle, err := h.UpdateUseCase.UpdateIsOnline(id, isOnline)
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeArticleJsonResponse(w, updatedArticle)
}
type UpdateArticleBody struct {
Body string `json:"body" validate:"required,max=10000"`
}
func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
var localArticle UpdateArticleBody
if err := json.NewDecoder(r.Body).Decode(&localArticle); err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(localArticle); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
updatedArticle, err := h.UpdateUseCase.UpdateBody(id, localArticle.Body)
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeArticleJsonResponse(w, updatedArticle)
}
func (h *Handler) PublishArticle(w http.ResponseWriter, r *http.Request) {
h.setIsOnline(w, r, true)
}
func (h *Handler) UnpublishArticle(w http.ResponseWriter, r *http.Request) {
h.setIsOnline(w, r, false)
}
func (h *Handler) callerIsVerified(r *http.Request) bool {
userId, ok := claims.UserId(r)
if !ok {
return false
}
currentUser, err := h.GetUserUseCase.GetUser(userId)
if err != nil {
return false
}
return currentUser.IsVerified
}
func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
var articles []domain.Article
var err error
if h.callerIsVerified(r) {
articles, err = h.ListUseCase.ListAllArticles()
} else {
articles, err = h.ListUseCase.ListOnlineArticles()
}
if err != nil {
http.Error(w, "Failed to retrieve articles", http.StatusInternalServerError)
return
}
articlesJson, err := json.Marshal(articles)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articlesJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
if err := h.DeleteUseCase.DeleteArticle(id); err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
}
http.Error(w, "Failed to delete article", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
+142
View File
@@ -0,0 +1,142 @@
package auth
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/go-chi/jwtauth/v5"
)
type LoginHandler struct {
Token *jwtauth.JWTAuth
CreateUseCase *useCases.CreateUserUseCase
AuthenticateUseCase *useCases.AuthenticateUserUseCase
IsProduction bool
}
type RegisterCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
Email string `json:"email" validate:"required,email"`
}
type LoginCredentials struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Password string `json:"password" validate:"required,min=8"`
}
func NewLoginHandler(token *jwtauth.JWTAuth, create *useCases.CreateUserUseCase, authenticate *useCases.AuthenticateUserUseCase, isProduction bool) *LoginHandler {
return &LoginHandler{
Token: token,
CreateUseCase: create,
AuthenticateUseCase: authenticate,
IsProduction: isProduction,
}
}
func removeJwtCookie(w http.ResponseWriter, isProduction bool) {
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: "",
Expires: time.Now(),
MaxAge: -1,
Secure: isProduction,
HttpOnly: true,
Path: "/",
})
}
func (h *LoginHandler) SetJwtCookie(w *http.ResponseWriter, u user.User) error {
_, tokenString, err := h.Token.Encode(map[string]any{"user_id": u.ID, "token_version": u.TokenVersion})
if err != nil {
return err
}
http.SetCookie(*w, &http.Cookie{
Name: "jwt",
Value: tokenString,
Expires: time.Now().Add(24 * time.Hour),
Secure: h.IsProduction,
HttpOnly: true,
Path: "/",
})
return nil
}
func (h *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
var credentials LoginCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
dbUser, err := h.AuthenticateUseCase.Authenticate(credentials.Username, credentials.Password)
if err != nil {
http.Error(w, apperr.LogsErrorMessage, http.StatusForbidden)
return
}
_ = h.SetJwtCookie(&w, dbUser)
message, _ := json.Marshal(map[string]any{"message": "User logged in! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func (h *LoginHandler) Register(w http.ResponseWriter, r *http.Request) {
var credentials RegisterCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
createdUser, err := h.CreateUseCase.CreateUser(useCases.CreateUserCommand{
Username: credentials.Username,
Password: credentials.Password,
Email: credentials.Email,
})
if err != nil {
if errors.Is(err, user.ErrRegistrationClosed) {
http.Error(w, apperr.RegistrationClosedErrorMessage, http.StatusForbidden)
return
}
http.Error(w, apperr.RegistrationFailedErrorMessage, http.StatusInternalServerError)
return
}
_ = h.SetJwtCookie(&w, createdUser)
message, _ := json.Marshal(map[string]any{"message": "User registered! HTTPonly jwt cookie created"})
_, _ = w.Write(message)
}
func (h *LoginHandler) RegistrationStatus(w http.ResponseWriter, _ *http.Request) {
body, _ := json.Marshal(map[string]bool{"open": h.CreateUseCase.IsRegistrationOpen()})
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(body)
}
func (h *LoginHandler) Logout(w http.ResponseWriter, _ *http.Request) {
removeJwtCookie(w, h.IsProduction)
message, _ := json.Marshal(map[string]any{"message": "User logged out! HTTPonly jwt cookie deleted"})
_, _ = w.Write(message)
}
+47
View File
@@ -0,0 +1,47 @@
package auth
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/api/pkg/claims"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"net/http"
)
type MeHandler struct {
GetUseCase *useCases.GetUserUseCase
}
func NewMeHandler(getUser *useCases.GetUserUseCase) *MeHandler {
return &MeHandler{
GetUseCase: getUser,
}
}
func (h *MeHandler) Me(w http.ResponseWriter, r *http.Request) {
userId, ok := claims.UserId(r)
if !ok {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
loadedUser, err := h.GetUseCase.GetUser(userId)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
body, err := json.Marshal(newPublicUser(loadedUser))
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(body)
}
+81
View File
@@ -0,0 +1,81 @@
package auth
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"net/http"
)
type PasswordResetHandler struct {
RequestPasswordResetUseCase *useCases.RequestPasswordResetUseCase
ResetPasswordUseCase *useCases.ResetPasswordUseCase
}
type RequestPasswordResetCredentials struct {
Email string `json:"email" validate:"required,email"`
}
type ResetPasswordCredentials struct {
Email string `json:"email" validate:"required,email"`
Code string `json:"code" validate:"required"`
Password string `json:"password" validate:"required,min=8"`
}
func NewPasswordResetHandler(requestPasswordReset *useCases.RequestPasswordResetUseCase, resetPassword *useCases.ResetPasswordUseCase) *PasswordResetHandler {
return &PasswordResetHandler{
RequestPasswordResetUseCase: requestPasswordReset,
ResetPasswordUseCase: resetPassword,
}
}
func (h *PasswordResetHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
var credentials RequestPasswordResetCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = h.RequestPasswordResetUseCase.RequestPasswordReset(credentials.Email)
message, _ := json.Marshal(map[string]any{"message": apperr.PasswordResetRequestedMessage})
_, _ = w.Write(message)
}
func (h *PasswordResetHandler) ResetPassword(w http.ResponseWriter, r *http.Request) {
var credentials ResetPasswordCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.ResetPasswordUseCase.ResetPassword(credentials.Email, credentials.Code, credentials.Password)
if err != nil {
if errors.Is(err, user.ErrInvalidPasswordResetCode) || errors.Is(err, user.ErrPasswordResetCodeExpired) {
http.Error(w, apperr.PasswordResetFailedErrorMessage, http.StatusForbidden)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
message, _ := json.Marshal(map[string]any{"message": "Your password has been reset. You can now log in."})
_, _ = w.Write(message)
}
+119
View File
@@ -0,0 +1,119 @@
package auth
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/api/pkg/claims"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"net/http"
"time"
)
type VerificationHandler struct {
VerifyUseCase *useCases.VerifyUserUseCase
CancelUseCase *useCases.CancelRegistrationUseCase
IsProduction bool
}
type VerifyCredentials struct {
Code string `json:"code" validate:"required"`
}
type PublicUser struct {
ID uint32 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsVerified bool `json:"is_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func newPublicUser(u user.User) PublicUser {
return PublicUser{
ID: u.ID,
Username: u.Username,
Email: u.Email,
IsVerified: u.IsVerified,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
func NewVerificationHandler(verify *useCases.VerifyUserUseCase, cancel *useCases.CancelRegistrationUseCase, isProduction bool) *VerificationHandler {
return &VerificationHandler{
VerifyUseCase: verify,
CancelUseCase: cancel,
IsProduction: isProduction,
}
}
func (h *VerificationHandler) Verify(w http.ResponseWriter, r *http.Request) {
var credentials VerifyCredentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
err = validator.Validate.Struct(credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
userId, ok := claims.UserId(r)
if !ok {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
verifiedUser, err := h.VerifyUseCase.Verify(userId, credentials.Code)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
if errors.Is(err, user.ErrInvalidVerificationCode) || errors.Is(err, user.ErrVerificationCodeExpired) {
http.Error(w, apperr.VerificationFailedErrorMessage, http.StatusForbidden)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
body, err := json.Marshal(newPublicUser(verifiedUser))
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
_, _ = w.Write(body)
}
func (h *VerificationHandler) CancelRegistration(w http.ResponseWriter, r *http.Request) {
userId, ok := claims.UserId(r)
if !ok {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
err := h.CancelUseCase.CancelRegistration(userId)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
if errors.Is(err, user.ErrAlreadyVerified) {
http.Error(w, apperr.AlreadyVerifiedErrorMessage, http.StatusForbidden)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
removeJwtCookie(w, h.IsProduction)
message, _ := json.Marshal(map[string]any{"message": "Registration cancelled! Account deleted and HTTPonly jwt cookie cleared"})
_, _ = w.Write(message)
}
+104
View File
@@ -0,0 +1,104 @@
package frontend
import (
"RenewCMS/internal/infrastructure/assets"
"io"
"io/fs"
"net/http"
"path"
"strings"
"time"
"github.com/go-chi/chi/v5"
)
const hashedAssetsPrefix = "assets/"
const (
hashedAssetCacheControl = "public, max-age=31536000, immutable"
staticFileCacheControl = "public, max-age=3600"
indexCacheControl = "no-store, no-cache, must-revalidate"
)
const devNotice = "RenewCMS was built with -tags nofrontend: the SPA is served by the Vite dev server (http://localhost:5173)."
type spaHandler struct {
staticFS fs.FS
modTime time.Time
}
func NewFrontendRouter() http.Handler {
r := chi.NewRouter()
if assets.Dist == nil {
r.Get("/*", serveDevNotice)
return r
}
handler := &spaHandler{
staticFS: assets.Dist,
modTime: time.Now(),
}
r.Get("/*", handler.ServeHTTP)
return r
}
func serveDevNotice(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
_, _ = io.WriteString(w, devNotice)
}
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := h.cleanPath(r.URL.Path)
if h.fileExists(path) {
h.serveAsset(w, r, path)
return
}
h.serveIndex(w, r)
}
func (h *spaHandler) cleanPath(p string) string {
return strings.TrimPrefix(path.Clean("/"+p), "/")
}
func (h *spaHandler) fileExists(path string) bool {
if path == "" || path == "." {
return false
}
f, err := h.staticFS.Open(path)
if err != nil {
return false
}
defer f.Close()
stat, err := f.Stat()
return err == nil && !stat.IsDir()
}
func (h *spaHandler) serveAsset(w http.ResponseWriter, r *http.Request, path string) {
if strings.HasPrefix(path, hashedAssetsPrefix) {
w.Header().Set("Cache-Control", hashedAssetCacheControl)
} else {
w.Header().Set("Cache-Control", staticFileCacheControl)
}
http.FileServer(http.FS(h.staticFS)).ServeHTTP(w, r)
}
func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) {
f, err := h.staticFS.Open("index.html")
if err != nil {
http.Error(w, "Index not found", http.StatusNotFound)
return
}
defer f.Close()
content, ok := f.(io.ReadSeeker)
if !ok {
http.Error(w, "File not seekable", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", indexCacheControl)
http.ServeContent(w, r, "index.html", h.modTime, content)
}
+89
View File
@@ -0,0 +1,89 @@
package image
import (
"RenewCMS/api/pkg/apperr"
domainArticle "RenewCMS/internal/domain/article"
domainImage "RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type Handler struct {
CreateUseCase *useCases.CreateImageUseCase
GetUseCase *useCases.GetArticleUseCase
UpdateUseCase *useCases.UpdateArticleUseCase
}
func NewHandler(create *useCases.CreateImageUseCase, get *useCases.GetArticleUseCase, update *useCases.UpdateArticleUseCase) *Handler {
return &Handler{
CreateUseCase: create,
GetUseCase: get,
UpdateUseCase: update,
}
}
func writeInternalServerError(w http.ResponseWriter) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
func writeArticleError(w http.ResponseWriter, err error) {
if errors.Is(err, domainArticle.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
}
writeInternalServerError(w)
}
func (h *Handler) PostImage(w http.ResponseWriter, r *http.Request) {
articleId, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
if _, err := h.GetUseCase.GetArticle(uint32(articleId)); err != nil {
writeArticleError(w, err)
return
}
file, fileHeader, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
newImage, err := h.CreateUseCase.CreateImage(domainImage.ImageInput{
Content: file,
Filename: fileHeader.Filename,
Size: fileHeader.Size,
ContentType: fileHeader.Header.Get("Content-Type"),
})
if err != nil {
if errors.Is(err, domainImage.ErrImageTypeNotAllowed) || errors.Is(err, domainImage.ErrImageTooLarge) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeInternalServerError(w)
return
}
if err := h.UpdateUseCase.AddImage(uint32(articleId), newImage.ID); err != nil {
writeArticleError(w, err)
return
}
responseJson, err := json.Marshal(map[string]any{"location": newImage.Path})
if err != nil {
writeInternalServerError(w)
return
}
w.WriteHeader(http.StatusCreated)
_, _ = w.Write(responseJson)
}
@@ -0,0 +1,98 @@
package staticimages
import (
"io"
"io/fs"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/go-chi/chi/v5"
)
const cacheControl = "public, max-age=31536000, immutable"
const svgContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; sandbox"
const fallbackContentType = "application/octet-stream"
var contentTypeExtensions = map[string]string{
".png": "image/png",
".jpeg": "image/jpeg",
".webp": "image/webp",
".svg": "image/svg+xml",
}
type Handler struct {
uploadDir fs.FS
modTime time.Time
}
func NewHandler(uploadDir string) *Handler {
return &Handler{
uploadDir: os.DirFS(uploadDir),
modTime: time.Now(),
}
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
cleaned := cleanPath(requestedPath(r))
file, stat, ok := h.openFile(cleaned)
if !ok {
http.NotFound(w, r)
return
}
defer file.Close()
content, ok := file.(io.ReadSeeker)
if !ok {
http.Error(w, "File not seekable", http.StatusInternalServerError)
return
}
contentType := contentTypeFor(cleaned)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", cacheControl)
if contentType == "image/svg+xml" {
w.Header().Set("Content-Security-Policy", svgContentSecurityPolicy)
}
http.ServeContent(w, r, cleaned, stat.ModTime(), content)
}
func requestedPath(r *http.Request) string {
if rctx := chi.RouteContext(r.Context()); rctx != nil && rctx.RoutePath != "" {
return rctx.RoutePath
}
return r.URL.Path
}
func cleanPath(p string) string {
return strings.TrimPrefix(path.Clean("/"+p), "/")
}
func (h *Handler) openFile(p string) (fs.File, fs.FileInfo, bool) {
if p == "" || p == "." {
return nil, nil, false
}
file, err := h.uploadDir.Open(p)
if err != nil {
return nil, nil, false
}
stat, err := file.Stat()
if err != nil || stat.IsDir() {
file.Close()
return nil, nil, false
}
return file, stat, true
}
func contentTypeFor(p string) string {
ext := path.Ext(p)
if contentType, ok := contentTypeExtensions[ext]; ok {
return contentType
}
return fallbackContentType
}
+79
View File
@@ -0,0 +1,79 @@
package middleware
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/api/pkg/claims"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"net/http"
)
func JsonContentTypeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
func resolveCurrentUser(getUser *useCases.GetUserUseCase, w http.ResponseWriter, r *http.Request) (user.User, bool) {
userId, ok := claims.UserId(r)
if !ok {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return user.User{}, false
}
currentUser, err := getUser.GetUser(userId)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return user.User{}, false
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return user.User{}, false
}
return currentUser, true
}
func RequireCurrentToken(getUser *useCases.GetUserUseCase) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, ok := resolveCurrentUser(getUser, w, r)
if !ok {
return
}
tokenVersion, ok := claims.TokenVersion(r)
if !ok {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
if tokenVersion != currentUser.TokenVersion {
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
func RequireVerifiedUser(getUser *useCases.GetUserUseCase) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, ok := resolveCurrentUser(getUser, w, r)
if !ok {
return
}
if !currentUser.IsVerified {
http.Error(w, apperr.VerificationRequiredErrorMessage, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
+60
View File
@@ -0,0 +1,60 @@
package middleware
import (
"RenewCMS/api/pkg/apperr"
"net/http"
"strconv"
"sync"
"time"
)
type rateLimitWindow struct {
start time.Time
count int
}
func NewRateLimiter(maxAttempts int, window time.Duration) func(http.Handler) http.Handler {
var mu sync.Mutex
windows := make(map[string]*rateLimitWindow)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
retryAfter, allowed := recordAttempt(&mu, windows, r.RemoteAddr, maxAttempts, window)
if !allowed {
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
http.Error(w, apperr.TooManyRequestsErrorMessage, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
func recordAttempt(mu *sync.Mutex, windows map[string]*rateLimitWindow, key string, maxAttempts int, window time.Duration) (int, bool) {
mu.Lock()
defer mu.Unlock()
now := time.Now()
current, exists := windows[key]
if !exists || now.Sub(current.start) >= window {
current = &rateLimitWindow{start: now}
windows[key] = current
}
current.count++
if current.count > maxAttempts {
return retryAfterSeconds(window - now.Sub(current.start)), false
}
return 0, true
}
func retryAfterSeconds(remaining time.Duration) int {
seconds := int(remaining / time.Second)
if remaining%time.Second > 0 {
seconds++
}
if seconds < 0 {
return 0
}
return seconds
}
+44
View File
@@ -0,0 +1,44 @@
package middleware
import (
"net"
"net/http"
)
func TrustedProxyRealIP(trustedProxies []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
realIP := r.Header.Get("X-Real-IP")
if realIP != "" && isTrustedProxy(r.RemoteAddr, trustedProxies) {
r.RemoteAddr = withRewrittenHost(r.RemoteAddr, realIP)
}
next.ServeHTTP(w, r)
})
}
}
func isTrustedProxy(remoteAddr string, trustedProxies []string) bool {
host := hostOf(remoteAddr)
for _, trusted := range trustedProxies {
if host == trusted {
return true
}
}
return false
}
func hostOf(remoteAddr string) string {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return remoteAddr
}
return host
}
func withRewrittenHost(remoteAddr string, newHost string) string {
_, port, err := net.SplitHostPort(remoteAddr)
if err != nil {
return newHost
}
return net.JoinHostPort(newHost, port)
}
+16
View File
@@ -0,0 +1,16 @@
package apperr
const LogsErrorMessage = "Access to the requested resource is forbidden due to incorrect password and/or username."
const BodyErrorMessage = "The request cannot be processed due to a mismatch in the format of the body."
const IdUint32ErrorMessage = "The server expects the ID to be in the format of an unsigned 32-bit integer (uint32)."
const ArticleNotFoundErrorMessage = "Article not found"
const ArticleTitleErrorMessage = "Article title must be between 3 and 200 characters."
const RegistrationClosedErrorMessage = "You are not allowed to create a user. Log in or reset database."
const RegistrationFailedErrorMessage = "Registration failed. Please try again later."
const UnauthorizedErrorMessage = "You must be logged in to access this resource."
const VerificationFailedErrorMessage = "The verification code is invalid or has expired."
const AlreadyVerifiedErrorMessage = "This account is already verified and cannot be cancelled this way."
const VerificationRequiredErrorMessage = "You must verify your account to access this resource."
const PasswordResetRequestedMessage = "If an account exists for that email, a password reset link has been sent."
const PasswordResetFailedErrorMessage = "The password reset code is invalid or has expired."
const TooManyRequestsErrorMessage = "Too many requests. Please try again later."
+50
View File
@@ -0,0 +1,50 @@
package claims
import (
"math"
"net/http"
"github.com/go-chi/jwtauth/v5"
)
func UserId(r *http.Request) (uint32, bool) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return 0, false
}
rawUserId, ok := claims["user_id"]
if !ok {
return 0, false
}
userIdFloat, ok := rawUserId.(float64)
if !ok {
return 0, false
}
if userIdFloat < 0 || userIdFloat > math.MaxUint32 || userIdFloat != math.Trunc(userIdFloat) {
return 0, false
}
return uint32(userIdFloat), true
}
func TokenVersion(r *http.Request) (uint32, bool) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
return 0, false
}
rawTokenVersion, ok := claims["token_version"]
if !ok {
return 0, false
}
tokenVersionFloat, ok := rawTokenVersion.(float64)
if !ok {
return 0, false
}
return uint32(tokenVersionFloat), true
}
@@ -1,4 +1,4 @@
package api
package validator
import "github.com/go-playground/validator/v10"
+148
View File
@@ -0,0 +1,148 @@
package api
import (
"RenewCMS/api/handlers/article"
"RenewCMS/api/handlers/auth"
"RenewCMS/api/handlers/frontend"
"RenewCMS/api/handlers/image"
"RenewCMS/api/handlers/staticimages"
"RenewCMS/api/middleware"
"RenewCMS/internal/infrastructure/config"
"encoding/json"
"net/http"
"github.com/MadAppGang/httplog"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/jwtauth/v5"
)
func enableLoggingIfDev(r *chi.Mux, routerName string, environment string) {
if environment == "development" {
r.Use(httplog.LoggerWithName(routerName))
}
}
func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
_, _ = w.Write(msg)
}
func NewArticleRouter(container *UseCases) http.Handler {
r := chi.NewRouter()
articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase, container.UpdateArticleUseCase, container.GetUserUseCase)
imageHandler := image.NewHandler(container.CreateImageUseCase, container.GetArticleUseCase, container.UpdateArticleUseCase)
r.Group(func(r chi.Router) {
r.Get("/", articleHandler.ListArticles)
r.Get("/{id}", articleHandler.GetArticle)
})
r.Group(func(r chi.Router) {
r.Use(jwtauth.Authenticator(container.Token))
r.Use(middleware.RequireCurrentToken(container.GetUserUseCase))
r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase))
r.Post("/", articleHandler.PostArticle)
r.Delete("/{id}", articleHandler.DeleteArticle)
r.Patch("/{id}", articleHandler.UpdateArticleBody)
r.Patch("/{id}/publish", articleHandler.PublishArticle)
r.Patch("/{id}/unpublish", articleHandler.UnpublishArticle)
r.Post("/{id}/image", imageHandler.PostImage)
})
return r
}
func NewAuthRouter(container *UseCases, isProduction bool, rateLimit config.AuthRateLimitSettings, trustedProxies []string) http.Handler {
r := chi.NewRouter()
loginHandler := auth.NewLoginHandler(container.Token, container.CreateUserUseCase, container.AuthenticateUserUseCase, isProduction)
verificationHandler := auth.NewVerificationHandler(container.VerifyUserUseCase, container.CancelRegistrationUseCase, isProduction)
meHandler := auth.NewMeHandler(container.GetUserUseCase)
passwordResetHandler := auth.NewPasswordResetHandler(container.RequestPasswordResetUseCase, container.ResetPasswordUseCase)
r.Post("/register", loginHandler.Register)
r.Get("/registration", loginHandler.RegistrationStatus)
r.Group(func(r chi.Router) {
r.Use(middleware.TrustedProxyRealIP(trustedProxies))
r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window))
r.Post("/login", loginHandler.Login)
})
r.Post("/logout", loginHandler.Logout)
r.Group(func(r chi.Router) {
r.Use(middleware.TrustedProxyRealIP(trustedProxies))
r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window))
r.Post("/password-reset", passwordResetHandler.RequestPasswordReset)
})
r.Post("/password-reset/confirm", passwordResetHandler.ResetPassword)
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(container.Token))
r.Use(jwtauth.Authenticator(container.Token))
r.Use(middleware.RequireCurrentToken(container.GetUserUseCase))
r.Post("/verify", verificationHandler.Verify)
r.Delete("/register", verificationHandler.CancelRegistration)
r.Get("/me", meHandler.Me)
})
return r
}
func InitBackendRoutes(container *UseCases, settings config.Settings) *chi.Mux {
r := chi.NewRouter()
enableLoggingIfDev(r, "backend", settings.Environment)
r.Use(chimiddleware.GetHead)
r.Use(middleware.JsonContentTypeMiddleware)
r.Get("/", GetHelloWorld)
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(container.Token))
r.Mount("/article", NewArticleRouter(container))
})
r.Mount("/auth", NewAuthRouter(container, settings.IsProduction(), settings.AuthRateLimit, settings.TrustedProxies))
return r
}
func InitFrontendRoutes(environment string) *chi.Mux {
r := chi.NewRouter()
enableLoggingIfDev(r, "frontend", environment)
r.Mount("/", frontend.NewFrontendRouter())
return r
}
func GetHealth(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("ok"))
}
func enableCorsIfConfigured(r *chi.Mux, origins []string) {
if len(origins) == 0 {
return
}
r.Use(cors.Handler(cors.Options{
AllowedOrigins: origins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}))
}
func InitRoutes(container *UseCases, settings config.Settings) *chi.Mux {
backend := InitBackendRoutes(container, settings)
frontend := InitFrontendRoutes(settings.Environment)
apiRouter := chi.NewRouter()
enableCorsIfConfigured(apiRouter, settings.CorsAllowedOrigins)
apiRouter.Use(chimiddleware.GetHead)
apiRouter.Get("/healthz", GetHealth)
apiRouter.Mount("/v1", backend)
apiRouter.Mount("/static/uploadedImages", staticimages.NewHandler(settings.UploadDir))
apiRouter.Mount("/", frontend)
return apiRouter
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol class="bi bi-0-circle" viewBox="0 0 16 16" id="0-circle"><path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/></symbol><symbol class="bi bi-0-circle-fill" viewBox="0 0 16 16" id="0-circle-fill"><path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/></symbol><symbol class="bi bi-0-square" viewBox="0 0 16 16" id="0-square"><path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/></symbol><symbol class="bi bi-0-square-fill" viewBox="0 0 16 16" id="0-square-fill"><path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/><path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/></symbol><symbol class="bi bi-1-circle" viewBox="0 0 16 16" id="1-circle"><path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/></symbol><symbol class="bi bi-1-circle-fill" viewBox="0 0 16 16" id="1-circle-fill"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/></symbol><symbol class="bi bi-1-square" viewBox="0 0 16 16" id="1-square"><path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/></symbol><symbol class="bi bi-1-square-fill" viewBox="0 0 16 16" id="1-square-fill"><path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/></symbol><symbol class="bi bi-123" viewBox="0 0 16 16" id="123"><path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/></symbol><symbol class="bi bi-2-circle" viewBox="0 0 16 16" id="2-circle"><path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/></symbol><symbol class="bi bi-2-circle-fill" viewBox="0 0 16 16" id="2-circle-fill"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/></symbol><symbol class="bi bi-2-square" viewBox="0 0 16 16" id="2-square"><path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/></symbol><symbol class="bi bi-2-square-fill" viewBox="0 0 Line truncated

Before

Width:  |  Height:  |  Size: 1.0 MiB

-6
View File
@@ -1,6 +0,0 @@
@charset "UTF-8";/*!
* Bootstrap v5.3.1 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--Line truncated
/*# sourceMappingURL=bootstrap.min.css.map */
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_focus-ring.scss","../../scss/helpers/_icon-link.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"iBACE;;;;ACDF,MCOA,sBDEI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAIA,uBAAA,QAAA,yBAAA,QAAA,uBAAA,QAAA,oBAAA,QAAA,uBAAA,QAAA,sBAAA,QAAA,qBAAA,QAAA,oBAAA,QAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,KAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAOA,sBAAA,0BE2OI,oBAAA,KFzOJ,sBAAA,IACA,sBAAA,IAKA,gBAAA,QACA,oBAAA,EAAA,CAAA,EAAA,CAAA,GACA,aAAA,KACA,iBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,oBAAA,KACA,wBAAA,CAAA,CAAA,CAAA,CAAA,EAEA,qBAAA,uBACA,yBAAA,EAAA,CAAA,EAAA,CAAA,GACA,kBAAA,QACA,sBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,oBAAA,sBACA,wBAAA,EAAA,CAAA,EAAA,CAAA,GACA,iBAAA,QACA,qBAAA,GAAA,CAAA,GAAA,CAAA,IAGA,mBAAA,QAEA,gBAAA,QACA,oBAAA,EAAA,CAAA,GAAA,CAAA,IACA,qBAAA,UAEA,sBAAA,QACA,0BAAA,EAAA,CAAA,EAAA,CAAA,IAMA,gBAAA,QACA,kBAAA,QAGA,kBAAA,IACA,kBAAA,MACA,kBAAA,QACA,8BAAA,qBAEA,mBAAA,SACA,sBAAA,QACA,sBAAA,OACA,sBAAA,KACA,uBAAA,KACA,uBAAA,4BACA,wBAAA,MAGA,gBAAA,EAAA,OAAA,KAAA,oBACA,mBAAA,EAAA,SAAA,QAAA,qBACA,mBAAA,EAAA,KAAA,KAAA,qBACA,sBAAA,MAAA,EAAA,IAAA,IAAA,qBAIA,sBAAA,QACA,wBAAA,KACA,sBAAA,yBAIA,sBAAA,QACA,6BAAA,QACA,wBAAA,QACA,+BAAA,QG/GE,qBHqHA,aAAA,KAGA,gBAAA,QACA,oBAAA,GAAA,CAAA,GAAA,CAAA,IACA,aAAA,QACA,iBAAA,EAAA,CAAA,EAAA,CAAA,GAEA,oBAAA,KACA,wBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,qBAAA,0BACA,yBAAA,GAAA,CAAA,GAAA,CAAA,IACA,kBAAA,QACA,sBAAA,EAAA,CAAA,EAAA,CAAA,GAEA,oBAAA,yBACA,wBAAA,GAAA,CAAA,GAAA,CAAA,IACA,iBAAA,QACA,qBAAA,EAAA,CAAA,EAAA,CAAA,GAGE,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAIA,uBAAA,QAAA,yBAAA,QAAA,uBAAA,QAAA,oBAAA,QAAA,uBAAA,QAAA,sBAAA,QAAA,qBAAA,QAAA,oBAAA,QAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAGF,mBAAA,QAEA,gBAAA,QACA,sBAAA,QACA,oBAAA,GAAA,CAAA,GAAA,CAAA,IACA,0BAAA,GAAA,CAAA,GAAA,CAAA,IAEA,gBAAA,QAEALine truncated
-7
View File
@@ -1,7 +0,0 @@
/*!
* Bootstrap v5.3.1 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&PLine truncated
//# sourceMappingURL=bootstrap.min.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","call","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","map","join","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ArrowLeft","ArrowRight","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActive","order","defaultInterval","_keydown","_addTouchEvenLine truncated
Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

File diff suppressed because it is too large. Load diff
@@ -1 +0,0 @@
tinymce.IconManager.add("default",{icons:{"accessibility-check":'<svg width="24" height="24"><path d="M12 2a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2c0-1.1.9-2 2-2Zm8 7h-5v12c0 .6-.4 1-1 1a1 1 0 0 1-1-1v-5c0-.6-.4-1-1-1a1 1 0 0 0-1 1v5c0 .6-.4 1-1 1a1 1 0 0 1-1-1V9H4a1 1 0 1 1 0-2h16c.6 0 1 .4 1 1s-.4 1-1 1Z" fill-rule="nonzero"/></svg>',"accordion-toggle":'<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 15c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1h-6a1 1 0 0 1-1-1Z"/><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M4 15c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1-1-1Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12 19c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1h-6a1 1 0 0 1-1-1Z"/><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M4 19c0-.6.4-1 1-1h6c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1-1-1Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.3 7.3a1 1 0 0 1 1.4 0L16 9.6l2.3-2.3a1 1 0 1 1 1.4 1.4L16 12.4l-3.7-3.7a1 1 0 0 1 0-1.4ZM4.3 11.7a1 1 0 0 1 0-1.4L6.6 8 4.3 5.7a1 1 0 0 1 1.4-1.4L9.4 8l-3.7 3.7a1 1 0 0 1-1.4 0Z"/></svg>',accordion:'<svg width="24" height="24"><rect x="12" y="7" width="10" height="2" rx="1"/><rect x="12" y="11" width="10" height="2" rx="1"/><rect x="12" y="15" width="6" height="2" rx="1"/><path fill-rule="evenodd" clip-rule="evenodd" d="M2.3 7.3a1 1 0 0 1 1.4 0L6 9.6l2.3-2.3a1 1 0 0 1 1.4 1.4L6 12.4 2.3 8.7a1 1 0 0 1 0-1.4Z"/></svg>',"action-next":'<svg width="24" height="24"><path fill-rule="nonzero" d="M5.7 7.3a1 1 0 0 0-1.4 1.4l7.7 7.7 7.7-7.7a1 1 0 1 0-1.4-1.4L12 13.6 5.7 7.3Z"/></svg>',"action-prev":'<svg width="24" height="24"><path fill-rule="nonzero" d="M18.3 15.7a1 1 0 0 0 1.4-1.4L12 6.6l-7.7 7.7a1 1 0 0 0 1.4 1.4L12 9.4l6.3 6.3Z"/></svg>',addtag:'<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 5a2 2 0 0 1 1.6.8L21 12l-4.4 6.2a2 2 0 0 1-1.6.8h-3v-2h3l3.5-5L15 7H5v3H3V7c0-1.1.9-2 2-2h10Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6 12a1 1 0 0 0-1 1v2H3a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0-2H7v-2c0-.6-.4-1-1-1Z"/></svg>',"ai-prompt":'<svg width="24" height="24"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 6.7a1 1 0 0 0-1.4 0l-9.9 10a1 1 0 0 0 0 1.3l2.1 2.1c.4.4 1 .4 1.4 0l10-9.9c.3-.3.3-1 0-1.4l-2.2-2Zm1.4 2.8-2-2-3 2.7 2.2 2.2 2.8-2.9Z"/><path d="m18.5 7.3-.7-1.5-1.5-.8 1.5-.7.7-1.5.7 1.5 1.5.7-1.5.8-.7 1.5ZM18.5 16.5l-.7-1.6-1.5-.7 1.5-.7.7-1.6.7 1.6 1.5.7-1.5.7-.7 1.6ZM9.7 7.3 9 5.8 7.5 5 9 4.3l.7-1.5.7 1.5L12 5l-1.5.8-.7 1.5Z"/></g><defs><clipPath id="a"><path d="M0 0h24v24H0z"/></clipPath></defs></svg>',ai:'<svg width="24" height="24"><path fill-rule="evenodd" clip-rule="evenodd" d="M5 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3H5Zm6.8 11.5.5 1.2a68.3 68.3 0 0 0 .7 1.1l.4.1c.3 0 .5 0 .7-.3.2-.1.3-.3.3-.6l-.3-1-2.6-6.2a20.4 20.4 0 0 0-.5-1.3l-.5-.4-.7-.2c-.2 0-.5 0-.6.2-.2 0-.4.2-.5.4l-.3.6-.3.7L5.7 15l-.2.6-.1.4c0 .3 0 .5.3.7l.6.2c.3 0 .5 0 .7-.2l.4-1 .5-1.2h3.9ZM9.8 9l1.5 4h-3l1.5-4Zm5.6-.9v7.6c0 .4 0 .7.2 1l.7.2c.3 0 .6 0 .8-.3l.2-.9V8.1c0-.4 0-.7-.2-.9a1 1 0 0 0-.8-.3c-.2 0-.5.1-.7.3l-.2 1Z"/></svg>',"align-center":'<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm3 4h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H8a1 1 0 0 1 0-2Zm-3-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',"align-justify":'<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Zm0 4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',"align-left":'<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 4h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Zm0-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',"align-none":'<svg width="24" height="24"><path d="M14.2 5 13 7H5a1 1 0 1 1 0-2h9.2Zm4 0h.8a1 1 0 0 1 0 2h-2l1.2-2Zm-6.4 4-1.2 2H5a1 1 0 0 1 0-2h6.8Zm4 0H19a1 1 0 0 1 0 2h-4.4l1.2-2Zm-6.4 4-1.2 2H5a1 1 0 0 1 0-2h4.4Zm4 0H19a1 1 0 0 1 0 2h-6.8l1.2-2ZM7 17l-1.2 2H5a1 1 0 0 1 0-2h2Zm4 0h8a1 1 0 0 1 0 2H9.8l1.2-2Zm5.2-13.5 1.3.7-9.7 16.3-1.3-.7 9.7-16.3Z" fill-rule="evenodd"/></svg>',"align-right":'<svg width="24" height="24"><path d="M5 5h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 1 1 0-2Zm6 4h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm0 8h8c.6 0 1 .4 1 1s-.4 1-1 1h-8a1 1 0 0 1 0-2Zm-6-4h14c.6 0 1 .4 1 1s-.4 1-1 1H5a1 1 0 0 1 0-2Z" fill-rule="evenodd"/></svg>',"arrow-left":'<svg width="24" height="24"><path d="m5.6 13 12 6a1 1 0 0 0 1.4-1V6a1 1 0 0 0-1.4-.9l-12 6a1 1 0 0 0 0 1.8Z" fill-rule="evenodd"/></svg>',"arrow-right":'<svg width="24" height="24"><path d="m18.5 13-12 6A1 1 0 0 1 5 18V6a1 1 0 0 1 1.4-.9l12 6a1 1 0 0 1 0 1.8Z" fill-rule="evenodd"/></svg>',bold:'<svg width="24" height="24"><path d="M7.Line truncated
@@ -1,3 +0,0 @@
This is where language files should be placed.
Please DO NOT translate these directly, use this service instead: https://crowdin.com/project/tinymce
-6
View File
@@ -1,6 +0,0 @@
# Software License Agreement
**TinyMCE** – [<https://github.com/tinymce/tinymce>](https://github.com/tinymce/tinymce)
Copyright (c) 2024, Ephox Corporation DBA Tiny Technologies, Inc.
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-4
View File
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.ModelManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(o=n=e,(r=String).prototype.isPrototypeOf(o)||(null===(s=n.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var o,n,r,s})(t)===e,o=e=>t=>typeof t===e,n=e=>t=>e===t,r=t("string"),s=t("object"),l=t("array"),a=n(null),c=o("boolean"),i=n(void 0),m=e=>!(e=>null==e)(e),d=o("function"),u=o("number"),f=()=>{},g=e=>()=>e,h=e=>e,p=(e,t)=>e===t;function b(e,...t){return(...o)=>{const n=t.concat(o);return e.apply(null,n)}}const w=e=>t=>!e(t),v=e=>e(),y=g(!1),x=g(!0);class C{constructor(e,t){this.tag=e,this.value=t}static some(e){return new C(!0,e)}static none(){return C.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?C.some(e(this.value)):C.none()}bind(e){return this.tag?e(this.value):C.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:C.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return m(e)?C.some(e):C.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}C.singletonNone=new C(!1);const S=Array.prototype.slice,T=Array.prototype.indexOf,R=Array.prototype.push,D=(e,t)=>{return o=e,n=t,T.call(o,n)>-1;var o,n},O=(e,t)=>{for(let o=0,n=e.length;o<n;o++)if(t(e[o],o))return!0;return!1},k=(e,t)=>{const o=[];for(let n=0;n<e;n++)o.push(t(n));return o},E=(e,t)=>{const o=e.length,n=new Array(o);for(let r=0;r<o;r++){const o=e[r];n[r]=t(o,r)}return n},N=(e,t)=>{for(let o=0,n=e.length;o<n;o++)t(e[o],o)},B=(e,t)=>{const o=[],n=[];for(let r=0,s=e.length;r<s;r++){const s=e[r];(t(s,r)?o:n).push(s)}return{pass:o,fail:n}},_=(e,t)=>{const o=[];for(let n=0,r=e.length;n<r;n++){const r=e[n];t(r,n)&&o.push(r)}return o},z=(e,t,o)=>(((e,t)=>{for(let o=e.length-1;o>=0;o--)t(e[o],o)})(e,((e,n)=>{o=t(o,e,n)})),o),A=(e,t,o)=>(N(e,((e,n)=>{o=t(o,e,n)})),o),L=(e,t)=>((e,t,o)=>{for(let n=0,r=e.length;n<r;n++){const r=e[n];if(t(r,n))return C.some(r);if(o(r,n))break}return C.none()})(e,t,y),W=(e,t)=>{for(let o=0,n=e.length;o<n;o++)if(t(e[o],o))return C.some(o);return C.none()},M=e=>{const t=[];for(let o=0,n=e.length;o<n;++o){if(!l(e[o]))throw new Error("Arr.flatten item "+o+" was not an array, input: "+e);R.apply(t,e[o])}return t},j=(e,t)=>M(E(e,t)),P=(e,t)=>{for(let o=0,n=e.length;o<n;++o)if(!0!==t(e[o],o))return!1;return!0},I=(e,t)=>{const o={};for(let n=0,r=e.length;n<r;n++){const r=e[n];o[String(r)]=t(r,n)}return o},F=(e,t)=>t>=0&&t<e.length?C.some(e[t]):C.none(),H=e=>F(e,0),$=e=>F(e,e.length-1),V=(e,t)=>{for(let o=0;o<e.length;o++){const n=t(e[o],o);if(n.isSome())return n}return C.none()},q=Object.keys,U=Object.hasOwnProperty,G=(e,t)=>{const o=q(e);for(let n=0,r=o.length;n<r;n++){const r=o[n];t(e[r],r)}},K=(e,t)=>Y(e,((e,o)=>({k:o,v:t(e,o)}))),Y=(e,t)=>{const o={};return G(e,((e,n)=>{const r=t(e,n);o[r.k]=r.v})),o},J=(e,t)=>{const o=[];return G(e,((e,n)=>{o.push(t(e,n))})),o},Q=e=>J(e,h),X=(e,t)=>U.call(e,t),Z="undefined"!=typeof window?window:Function("return this;")(),ee=(e,t)=>((e,t)=>{let o=null!=t?t:Z;for(let t=0;t<e.length&&null!=o;++t)o=o[e[t]];return o})(e.split("."),t),te=Object.getPrototypeOf,oe=e=>{const t=ee("ownerDocument.defaultView",e);return s(e)&&((e=>((e,t)=>{const o=((e,t)=>ee(e,t))(e,t);if(null==o)throw new Error(e+" not available on this browser");return o})("HTMLElement",e))(t).prototype.isPrototypeOf(e)||/^HTML\w*Element$/.test(te(e).constructor.name))},ne=e=>e.dom.nodeName.toLowerCase(),re=e=>e.dom.nodeType,se=e=>t=>re(t)===e,le=e=>8===re(e)||"#comment"===ne(e),ae=e=>ce(e)&&oe(e.dom),ce=se(1),ie=se(3),me=se(9),de=se(11),ue=e=>t=>ce(t)&&ne(t)===e,fe=(e,t,o)=>{if(!(r(o)||c(o)||u(o)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",o,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,o+"")},ge=(e,t,o)=>{fe(e.dom,t,o)},he=(e,t)=>{const o=e.dom;G(t,((e,t)=>{fe(o,t,e)}))},pe=(e,t)=>{const o=e.dom.getAttribute(t);return null===o?void 0:o},be=(e,t)=>C.from(pe(e,t)),we=(e,t)=>{e.dom.removeAttribute(t)},ve=e=>A(e.dom.attributes,((e,t)=>(e[t.name]=t.value,e)),{}),ye=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},xe={fromHtml:(e,t)=>{const o=(t||document).createElement("div");if(o.innerHTML=e,!o.hasChildNodes()||o.childNodes.length>1){const t="HTML does not have a single root node";throw console.error(t,e),new Error(t)}return ye(o.childNodes[0])},fromTag:(e,t)=>{const o=(t||document).createElement(e);return Line truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");let t=0;const o=e=>t=>typeof t===e,n=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(o=n=e,(r=String).prototype.isPrototypeOf(o)||(null===(s=n.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var o,n,r,s})(e),r=o("boolean"),s=e=>null==e,a=e=>!s(e),i=o("function"),d=o("number"),l=e=>()=>e,c=(e,t)=>e===t,m=l(!1);class u{constructor(e,t){this.tag=e,this.value=t}static some(e){return new u(!0,e)}static none(){return u.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?u.some(e(this.value)):u.none()}bind(e){return this.tag?e(this.value):u.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:u.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return a(e)?u.some(e):u.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}u.singletonNone=new u(!1);const g=Array.prototype.indexOf,p=(e,t)=>{return o=e,n=t,g.call(o,n)>-1;var o,n},h=(e,t)=>{const o=e.length,n=new Array(o);for(let r=0;r<o;r++){const o=e[r];n[r]=t(o,r)}return n},f=(e,t)=>{for(let o=0,n=e.length;o<n;o++)t(e[o],o)},y=Object.keys;"undefined"!=typeof window?window:Function("return this;")();const v=e=>e.dom.nodeName.toLowerCase(),w=e=>e.dom.nodeType,b=e=>t=>w(t)===e,N=b(1),T=b(3),A=b(9),C=b(11),S=(e,t,o)=>{if(!(n(o)||r(o)||d(o)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",o,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,o+"")},x=(e,t)=>{const o=e.dom.getAttribute(t);return null===o?void 0:o},E=(e,t)=>u.from(x(e,t)),D=(e,t)=>{e.dom.removeAttribute(t)},M=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},P={fromHtml:(e,t)=>{const o=(t||document).createElement("div");if(o.innerHTML=e,!o.hasChildNodes()||o.childNodes.length>1){const t="HTML does not have a single root node";throw console.error(t,e),new Error(t)}return M(o.childNodes[0])},fromTag:(e,t)=>{const o=(t||document).createElement(e);return M(o)},fromText:(e,t)=>{const o=(t||document).createTextNode(e);return M(o)},fromDom:M,fromPoint:(e,t,o)=>u.from(e.dom.elementFromPoint(t,o)).map(M)},O=(e,t)=>{const o=e.dom;if(1!==o.nodeType)return!1;{const e=o;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},k=e=>1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType||0===e.childElementCount,B=O,R=(L=/^\s+|\s+$/g,e=>e.replace(L,""));var L;const $=e=>void 0!==e.style&&i(e.style.getPropertyValue),I=e=>A(e)?e:P.fromDom(e.dom.ownerDocument),V=e=>u.from(e.dom.parentNode).map(P.fromDom),j=e=>u.from(e.dom.nextSibling).map(P.fromDom),q=e=>h(e.dom.childNodes,P.fromDom),F=i(Element.prototype.attachShadow)&&i(Node.prototype.getRootNode)?e=>P.fromDom(e.dom.getRootNode()):I,H=e=>P.fromDom(e.dom.host),z=e=>{const t=T(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const o=t.ownerDocument;return(e=>{const t=F(e);return C(o=t)&&a(o.dom.host)?u.some(t):u.none();var o})(P.fromDom(t)).fold((()=>o.body.contains(t)),(n=z,r=H,e=>n(r(e))));var n,r},K=(e,t)=>$(e)?e.style.getPropertyValue(t):"",U=(e,t)=>{V(e).each((o=>{o.dom.insertBefore(t.dom,e.dom)}))},Y=(e,t)=>{j(e).fold((()=>{V(e).each((e=>{_(e,t)}))}),(e=>{U(e,t)}))},_=(e,t)=>{e.dom.appendChild(t.dom)},G=(e,t)=>{f(t,((o,n)=>{const r=0===n?e:t[n-1];Y(r,o)}))},J=(e,t)=>{let o=[];return f(q(e),(e=>{t(e)&&(o=o.concat([e])),o=o.concat(J(e,t))})),o},Q=(e,t,o)=>{let n=e.dom;const r=i(o)?o:m;for(;n.parentNode;){n=n.parentNode;const e=P.fromDom(n);if(t(e))return u.some(e);if(r(e))break}return u.none()},W=e=>{const t=e.dom;null!==t.parentNode&&t.parentNode.removeChild(t)},X=(e,t,o)=>Q(e,(e=>O(e,t)),o),Z=(e,t)=>((e,t)=>{const o=void 0===t?document:t.dom;return k(o)?u.none():u.from(o.querySelector(e)).map(P.fromDom)})(t,e),ee=((e,t)=>{const o=t=>e(t)?u.from(t.dom.nodeValue):u.none();return{get:t=>{if(!e(t))throw new Error("Can only get text value of a text node");return o(t).getOr("")},getOption:o,set:(t,o)=>{if(!e(t))throw new Error("Can only set raw text value of a text node");t.dom.nodeValue=o}}})(T);var te=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","li","table","thead","tbody","tfoot","caption","tr","td","th"Line truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),i=t=>null==t,l=t=>!i(t);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return l(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=t=>e=>l(e)&&t.test(e.nodeName),d=u(/^(OL|UL|DL)$/),g=u(/^(TH|TD)$/),c=t=>i(t)||"default"===t?"":t,h=(t,e)=>s=>((t,e)=>{const s=t.selection.getNode();return e({parents:t.dom.getParents(s),element:s}),t.on("NodeChange",e),()=>t.off("NodeChange",e)})(t,(r=>((t,r)=>{const n=t.selection.getStart(!0);s.setActive(((t,e,s)=>((t,e,s)=>{for(let e=0,n=t.length;e<n;e++){const n=t[e];if(d(r=n)&&!/\btox\-/.test(r.className))return a.some(n);if(s(n,e))break}var r;return a.none()})(e,0,g).exists((e=>e.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))))(t,r,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)&&t.selection.isEditable()})(t,n)&&t.selection.isEditable())})(t,r.parents))),m=(t,s,r,n,i,l)=>{l.length>1?((t,s,r,n,i,l)=>{t.ui.registry.addSplitButton(s,{tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(o.map(l,(t=>{const e="OL"===i?"num":"bull",s="disc"===t||"decimal"===t?"default":t,r=c(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,i,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return a.from(s)})(t);return s.map((t=>e===t)).getOr(!1)},onSetup:h(t,i)})})(t,s,r,n,i,l):((t,s,r,n,i,l)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",onSetup:h(t,i),onAction:()=>t.queryCommandState(n)||""===l?t.execCommand(n):e(t,i,l)})})(t,s,r,n,i,c(l[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{m(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),m(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((s,r)=>{e(t,"UL",r["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((s,r)=>{e(t,"OL",r["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the List Styles plugin.")}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=("allow_html_in_named_anchor",e=>e.options.get("allow_html_in_named_anchor"));const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact((()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),(e=>{o.each(e,(e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)}))}))})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())}))})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;o<t.length;o++){const n=t[o];c(n)&&n.attr("contenteditable",e)}},u=e=>t=>{const o=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",o),o(),()=>{e.off("NodeChange",o)}};e.add("anchor",(e=>{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",(()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))}))})(e),(e=>{e.addCommand("mceAnchor",(()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>{const o=e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind,n=u(e)(t);return()=>{o(),n()}}}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t,onSetup:u(e)})})(e),e.on("PreInit",(()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)}))}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),w=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),h=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),w);if(!h)return null;let v=h.container;const _=k.backwards(h.container,h.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),w),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(h.container,h.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=4&&b.substr(0,4)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),s=o("min_height"),i=o("max_height"),n=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),l=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},g=(e,t,o,s)=>{var i;const n=parseInt(null!==(i=e.getStyle(t,o,s))&&void 0!==i?i:"",10);return isNaN(n)?0:n},a=(e,o,r,c)=>{var d;const f=e.dom,u=e.getDoc();if(!u)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void l(e,!0);const m=u.documentElement,h=c?c():n(e),p=null!==(d=s(e))&&void 0!==d?d:e.getElement().offsetHeight;let y=p;const S=g(f,m,"margin-top",!0),v=g(f,m,"margin-bottom",!0);let C=m.offsetHeight+S+v+h;C<0&&(C=0);const b=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+b>p&&(y=C+b);const w=i(e);if(w&&y>w?(y=w,l(e,!0)):l(e,!1),y!==o.get()){const s=y-o.get();if(f.setStyle(e.getContainer(),"height",y+"px"),o.set(y),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===(null==e?void 0:e.type.toLowerCase())){const t=e;return!0===t.selection||!0===t.paste}return!1})(r)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&s<0&&a(e,o,r,c)}};e.add("autoresize",(e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const o=(e=>{let t=0;return{get:()=>t,set:e=>{t=e}}})();((e,t)=>{e.addCommand("mceAutoResize",(()=>{a(e,t)}))})(e,o),((e,o)=>{let s,i,l=()=>r(e);e.on("init",(i=>{s=0;const r=n(e),g=e.dom;g.setStyles(e.getDoc().documentElement,{height:"auto"}),t.browser.isEdge()||t.browser.isIE()?g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r,"min-height":0}):g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r}),a(e,o,i,l),s+=1})),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",(t=>{if(1===s)i=e.getContainer().offsetHeight,a(e,o,t,l),s+=1;else if(2===s){const t=i<e.getContainer().offsetHeight;if(t){const t=e.dom,o=e.getDoc();t.setStyles(o.documentElement,{"min-height":0}),t.setStyles(e.getBody(),{"min-height":"inherit"})}l=t?(0,()=>0):l,s+=1}else a(e,o,t,l)}))})(e,o)}}))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=("string",t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":e;var r,o,a,s})(t));const r=(void 0,t=>undefined===t);var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),l=i("autosave_restore_when_empty"),c=i("autosave_interval"),d=i("autosave_retention"),m=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},v=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},f=t=>{var e;const r=parseInt(null!==(e=a.getItem(m(t)+"time"))&&void 0!==e?e:"0",10)||0;return!((new Date).getTime()-r>d(t)&&(p(t,!1),1))},p=(t,e)=>{const r=m(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},g=t=>{const e=m(t);!v(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},y=t=>{var e;const r=m(t);f(t)&&(t.setContent(null!==(e=a.getItem(r+"draft"))&&void 0!==e?e:"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{e.setEnabled(f(t));const r=()=>e.setEnabled(f(t));return t.on("StoreDraft RestoreDraft RemoveDraft",r),()=>t.off("StoreDraft RestoreDraft RemoveDraft",r)};t.add("autosave",(t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",(t=>{let e;s.each(D.get(),(t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))})),e&&(t.preventDefault(),t.returnValue=e)}))})(t),(t=>{(t=>{const e=c(t);o.setEditorInterval(t,(()=>{g(t)}),e)})(t);const e=()=>{(t=>{t.undoManager.transact((()=>{y(t),p(t)})),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",(()=>{l(t)&&t.dom.isEmpty(t.getBody())&&y(t)})),(t=>({hasDraft:()=>f(t),storeDraft:()=>g(t),restoreDraft:()=>y(t),removeDraft:e=>p(t,e),isEmpty:e=>v(t,e)}))(t))))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=(e,t)=>{const r=((e,t)=>e.dispatch("insertCustomChar",{chr:t}))(e,t).chr;e.execCommand("mceInsertContent",!1,r)},r=e=>t=>e===t,a=("array",e=>"array"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(r=a=e,(n=String).prototype.isPrototypeOf(r)||(null===(i=a.constructor)||void 0===i?void 0:i.name)===n.name)?"string":t;var r,a,n,i})(e));const n=r(null),i=r(void 0),o=e=>"function"==typeof e,s=(!1,()=>false);class l{constructor(e,t){this.tag=e,this.value=t}static some(e){return new l(!0,e)}static none(){return l.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?l.some(e(this.value)):l.none()}bind(e){return this.tag?e(this.value):l.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:l.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return null==e?l.none():l.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}l.singletonNone=new l(!1);const c=Array.prototype.push,u=(e,t)=>{const r=e.length,a=new Array(r);for(let n=0;n<r;n++){const r=e[n];a[n]=t(r,n)}return a};var g=tinymce.util.Tools.resolve("tinymce.util.Tools");const h=e=>t=>t.options.get(e),m=h("charmap"),p=h("charmap_append"),d=g.isArray,f="User Defined",y=e=>{return d(e)?(t=e,g.grep(t,(e=>d(e)&&2===e.length))):"function"==typeof e?e():[];var t},b=e=>{const t=((e,t)=>{const r=m(e);r&&(t=[{name:f,characters:y(r)}]);const a=p(e);if(a){const e=g.grep(t,(e=>e.name===f));return e.length?(e[0].characters=[...e[0].characters,...y(a)],t):t.concat({name:f,characters:y(a)})}return t})(e,[{name:"Currency",characters:[[36,"dollar sign"],[162,"cent sign"],[8364,"euro sign"],[163,"pound sign"],[165,"yen sign"],[164,"currency sign"],[8352,"euro-currency sign"],[8353,"colon sign"],[8354,"cruzeiro sign"],[8355,"french franc sign"],[8356,"lira sign"],[8357,"mill sign"],[8358,"naira sign"],[8359,"peseta sign"],[8360,"rupee sign"],[8361,"won sign"],[8362,"new sheqel sign"],[8363,"dong sign"],[8365,"kip sign"],[8366,"tugrik sign"],[8367,"drachma sign"],[8368,"german penny symbol"],[8369,"peso sign"],[8370,"guarani sign"],[8371,"austral sign"],[8372,"hryvnia sign"],[8373,"cedi sign"],[8374,"livre tournois sign"],[8375,"spesmilo sign"],[8376,"tenge sign"],[8377,"indian rupee sign"],[8378,"turkish lira sign"],[8379,"nordic mark sign"],[8380,"manat sign"],[8381,"ruble sign"],[20870,"yen character"],[20803,"yuan character"],[22291,"yuan character, in hong kong and taiwan"],[22278,"yen/yuan character variant one"]]},{name:"Text",characters:[[169,"copyright sign"],[174,"registered sign"],[8482,"trade mark sign"],[8240,"per mille sign"],[181,"micro sign"],[183,"middle dot"],[8226,"bullet"],[8230,"three dot leader"],[8242,"minutes / feet"],[8243,"seconds / inches"],[167,"section sign"],[182,"paragraph sign"],[223,"sharp s / ess-zed"]]},{name:"Quotations",characters:[[8249,"single left-pointing angle quotation mark"],[8250,"single right-pointing angle quotation mark"],[171,"left pointing guillemet"],[187,"right pointing guillemet"],[8216,"left single quotation mark"],[8217,"right single quotation mark"],[8220,"left double quotation mark"],[8221,"right double quotation mark"],[8218,"single low-9 quotation mark"],[8222,"double low-9 quotation mark"],[60,"less-than sign"],[62,"greater-than sign"],[8804,"less-than or equal to"],[8805,"greater-than or equal to"],[8211,"en dash"],[8212,"em dash"],[175,"macron"],[8254,"overline"],[164,"currency sign"],[166,"broken bar"],[168,"diaeresis"],[161,"inverted exclamation mark"],[191,"turned question mark"],[710,"circumflex accent"],[732,"small tilde"],[176,"degree sign"],[8722,"minus sign"],[177,"plus-minus sign"],[247,"division sign"],[8260,"fraction slash"],[215,"multiplication sign"],[185,"superscript one"],[178,"superscript two"],[179,"superscript three"],[188,"fraction one quarter"],[189,"fraction one half"],[190,"fraction three quarters"]]},{name:"Mathematical",characters:[[402,"function / florin"],[8747,"integral"],[8721,"n-ary sumation"],[8734,"infinity"],[8730,"square root"],[8764,"similar to"],[8773,"approximately equal to"],[8776,"almost equal to"],[8800,"not equal to"],[8801,"identical to"],[8712,"element of"],[8713,"not an element of"],[8715,"contains as member"],[8719,"n-ary product"],[8743,"logical and"],[8744,"logical or"],[172,"not sign"],[8745,"intersection"],[8746,"union"],[8706,"partial differential"],[8704,"for all"],[8707,"there exists"],[8709,"diameter"Line truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}();
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>!(e=>null==e)(e),n=()=>{};class a{constructor(e,t){this.tag=e,this.value=t}static some(e){return new a(!0,e)}static none(){return a.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?a.some(e(this.value)):a.none()}bind(e){return this.tag?e(this.value):a.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:a.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return t(e)?a.some(e):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);var s=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils");const r="undefined"!=typeof window?window:Function("return this;")(),i=function(e,t,n){const a=window.Prism;window.Prism={manual:!0};var s=function(e){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,a={},s={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof r?new r(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++n}),e.__id},clone:function e(t,n){var a,r;switch(n=n||{},s.util.type(t)){case"Object":if(r=s.util.objId(t),n[r])return n[r];for(var i in a={},n[r]=a,t)t.hasOwnProperty(i)&&(a[i]=e(t[i],n));return a;case"Array":return r=s.util.objId(t),n[r]?n[r]:(a=[],n[r]=a,t.forEach((function(t,s){a[s]=e(t,n)})),a);default:return t}},getLanguage:function(e){for(;e;){var n=t.exec(e.className);if(n)return n[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,n){e.className=e.className.replace(RegExp(t,"gi"),""),e.classList.add("language-"+n)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(a){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(a.stack)||[])[1];if(e){var t=document.getElementsByTagName("script");for(var n in t)if(t[n].src==e)return t[n]}return null}},isActive:function(e,t,n){for(var a="no-"+t;e;){var s=e.classList;if(s.contains(t))return!0;if(s.contains(a))return!1;e=e.parentElement}return!!n}},languages:{plain:a,plaintext:a,text:a,txt:a,extend:function(e,t){var n=s.util.clone(s.languages[e]);for(var a in t)n[a]=t[a];return n},insertBefore:function(e,t,n,a){var r=(a=a||s.languages)[e],i={};for(var o in r)if(r.hasOwnProperty(o)){if(o==t)for(var l in n)n.hasOwnProperty(l)&&(i[l]=n[l]);n.hasOwnProperty(o)||(i[o]=r[o])}var u=a[e];return a[e]=i,s.languages.DFS(s.languages,(function(t,n){n===u&&t!=e&&(this[t]=i)})),i},DFS:function e(t,n,a,r){r=r||{};var i=s.util.objId;for(var o in t)if(t.hasOwnProperty(o)){n.call(t,o,t[o],a||o);var l=t[o],u=s.util.type(l);"Object"!==u||r[i(l)]?"Array"!==u||r[i(l)]||(r[i(l)]=!0,e(l,n,o,r)):(r[i(l)]=!0,e(l,n,null,r))}}},plugins:{},highlightAll:function(e,t){s.highlightAllUnder(document,e,t)},highlightAllUnder:function(e,t,n){var a={callback:n,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};s.hooks.run("before-highlightall",a),a.elements=Array.prototype.slice.apply(a.container.querySelectorAll(a.selector)),s.hooks.run("before-all-elements-highlight",a);for(var r,i=0;r=a.elements[i++];)s.highlightElement(r,!0===t,a.callback)},highlightElement:function(t,n,a){var r=s.util.getLanguage(t),i=s.languages[r];s.util.setLanguage(t,r);var o=t.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&s.util.setLanguage(o,r);var l={element:t,language:r,grammar:i,code:t.textContent};function u(e){l.highlightedCode=e,s.hooks.run("before-insert",l),l.element.innerHTML=l.highlightedCode,s.hooks.run("after-highlight",l),s.hooks.run("complete",l),a&&a.call(l.element)}if(s.hooks.run("before-sanity-check",l),(o=l.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!l.code)return s.hooks.run("complete",l),void(a&&a.call(l.element));if(s.hooks.run("before-highlight",l),l.grammar)if(n&&e.Worker){var c=new Worker(s.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:l.language,code:l.code,immediateClose:!0}))}else u(s.highlight(l.code,l.grammar,l.language));else u(s.util.encode(l.code))},highlight:function(e,t,n){var a={code:e,grammar:t,language:n};if(s.hooks.run("bLine truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>typeof e===t,o=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(o=r=t,(n=String).prototype.isPrototypeOf(o)||(null===(i=r.constructor)||void 0===i?void 0:i.name)===n.name)?"string":e;var o,r,n,i})(t),r=e("boolean"),n=t=>!(t=>null==t)(t),i=e("function"),s=e("number"),l=(!1,()=>false);class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return n(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=(t,e)=>{for(let o=0,r=t.length;o<r;o++)e(t[o],o)},c=t=>{if(null==t)throw new Error("Node cannot be null or undefined");return{dom:t}},d=c,h=(t,e)=>{const o=t.dom;if(1!==o.nodeType)return!1;{const t=o;if(void 0!==t.matches)return t.matches(e);if(void 0!==t.msMatchesSelector)return t.msMatchesSelector(e);if(void 0!==t.webkitMatchesSelector)return t.webkitMatchesSelector(e);if(void 0!==t.mozMatchesSelector)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")}};"undefined"!=typeof window?window:Function("return this;")();const m=t=>e=>(t=>t.dom.nodeType)(e)===t,g=m(1),f=m(3),v=m(9),y=m(11),p=(t,e)=>{t.dom.removeAttribute(e)},w=i(Element.prototype.attachShadow)&&i(Node.prototype.getRootNode)?t=>d(t.dom.getRootNode()):t=>v(t)?t:d(t.dom.ownerDocument),b=t=>d(t.dom.host),N=t=>{const e=f(t)?t.dom.parentNode:t.dom;if(null==e||null===e.ownerDocument)return!1;const o=e.ownerDocument;return(t=>{const e=w(t);return y(o=e)&&n(o.dom.host)?a.some(e):a.none();var o})(d(e)).fold((()=>o.body.contains(e)),(r=N,i=b,t=>r(i(t))));var r,i},S=t=>"rtl"===((t,e)=>{const o=t.dom,r=window.getComputedStyle(o).getPropertyValue(e);return""!==r||N(t)?r:((t,e)=>(t=>void 0!==t.style&&i(t.style.getPropertyValue))(t)?t.style.getPropertyValue(e):"")(o,e)})(t,"direction")?"rtl":"ltr",A=(t,e)=>((t,o)=>((t,e)=>{const o=[];for(let r=0,n=t.length;r<n;r++){const n=t[r];e(n,r)&&o.push(n)}return o})(((t,e)=>{const o=t.length,r=new Array(o);for(let n=0;n<o;n++){const o=t[n];r[n]=e(o,n)}return r})(t.dom.childNodes,d),(t=>h(t,e))))(t),E=("li",t=>g(t)&&"li"===t.dom.nodeName.toLowerCase());const T=(t,e,n)=>{u(e,(e=>{const c=d(e),m=E(c),f=((t,e)=>{return(e?(o=t,r="ol,ul",((t,e,o)=>{let n=t.dom;const s=i(o)?o:l;for(;n.parentNode;){n=n.parentNode;const t=d(n);if(h(t,r))return a.some(t);if(s(t))break}return a.none()})(o,0,n)):a.some(t)).getOr(t);var o,r,n})(c,m);var v;(v=f,(t=>a.from(t.dom.parentNode).map(d))(v).filter(g)).each((e=>{if(t.setStyle(f.dom,"direction",null),S(e)===n?p(f,"dir"):((t,e,n)=>{((t,e,n)=>{if(!(o(n)||r(n)||s(n)))throw console.error("Invalid call to Attribute.set. Key ",e,":: Value ",n,":: Element ",t),new Error("Attribute value was not simple");t.setAttribute(e,n+"")})(t.dom,e,n)})(f,"dir",n),S(f)!==n&&t.setStyle(f.dom,"direction",n),m){const e=A(f,"li[dir],li[style]");u(e,(e=>{p(e,"dir"),t.setStyle(e.dom,"direction",null)}))}}))}))},C=(t,e)=>{t.selection.isEditable()&&(T(t.dom,t.selection.getSelectedBlocks(),e),t.nodeChanged())},D=(t,e)=>o=>{const r=r=>{const n=d(r.element);o.setActive(S(n)===e),o.setEnabled(t.selection.isEditable())};return t.on("NodeChange",r),o.setEnabled(t.selection.isEditable()),()=>t.off("NodeChange",r)};t.add("directionality",(t=>{(t=>{t.addCommand("mceDirectionLTR",(()=>{C(t,"ltr")})),t.addCommand("mceDirectionRTL",(()=>{C(t,"rtl")}))})(t),(t=>{t.ui.registry.addToggleButton("ltr",{tooltip:"Left to right",icon:"ltr",onAction:()=>t.execCommand("mceDirectionLTR"),onSetup:D(t,"ltr")}),t.ui.registry.addToggleButton("rtl",{tooltip:"Right to left",icon:"rtl",onAction:()=>t.execCommand("mceDirectionRTL"),onSetup:D(t,"rtl")})})(t)}))}();
@@ -1 +0,0 @@
window.tinymce.Resource.add("tinymce.plugins.emoticons",{100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="💯" src="1f4af.png"/>',fitzpatrick_scale:false,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="🔢" src="1f522.png"/>',fitzpatrick_scale:false,category:"symbols"},grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😀" src="1f600.png"/>',fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😬" src="1f62c.png"/>',fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😁" src="1f601.png"/>',fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😂" src="1f602.png"/>',fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="🤣" src="1f923.png"/>',fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="🥳" src="1f973.png"/>',fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😃" src="1f603.png"/>',fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😄" src="1f604.png"/>',fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😅" src="1f605.png"/>',fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😆" src="1f606.png"/>',fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😇" src="1f607.png"/>',fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😉" src="1f609.png"/>',fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😊" src="1f60a.png"/>',fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="🙂" src="1f642.png"/>',fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="🙃" src="1f643.png"/>',fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="☺️" src="263a.png"/>',fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="😋" src="1f60b.pngLine truncated
@@ -1,3 +0,0 @@
// Source: npm package: emojilib
// Images provided by twemoji: https://github.com/twitter/twemoji
window.tinymce.Resource.add("tinymce.plugins.emoticons",{100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f4af}" src="1f4af.png"/>',fitzpatrick_scale:!1,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f522}" src="1f522.png"/>',fitzpatrick_scale:!1,category:"symbols"},grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f600}" src="1f600.png"/>',fitzpatrick_scale:!1,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f62c}" src="1f62c.png"/>',fitzpatrick_scale:!1,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f601}" src="1f601.png"/>',fitzpatrick_scale:!1,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f602}" src="1f602.png"/>',fitzpatrick_scale:!1,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f923}" src="1f923.png"/>',fitzpatrick_scale:!1,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f973}" src="1f973.png"/>',fitzpatrick_scale:!1,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f603}" src="1f603.png"/>',fitzpatrick_scale:!1,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f604}" src="1f604.png"/>',fitzpatrick_scale:!1,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f605}" src="1f605.png"/>',fitzpatrick_scale:!1,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f606}" src="1f606.png"/>',fitzpatrick_scale:!1,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f607}" src="1f607.png"/>',fitzpatrick_scale:!1,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f609}" src="1f609.png"/>',fitzpatrick_scale:!1,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f60a}" src="1f60a.png"/>',fitzpatrick_scale:!1,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f642}" src="1f642.png"/>',fitzpatrick_scale:!1,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u{1f643}" src="1f643.png"/>',fitzpatrick_scale:!1,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggable="false" alt="\u263a\ufe0f" src="263a.png"/>',fitzpatrick_scale:!1,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:'<img data-emoticon="true" style="width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em" draggaLine truncated
@@ -1 +0,0 @@
window.tinymce.Resource.add("tinymce.plugins.emoticons",{grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:"😀",fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:"😬",fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:"😁",fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:"😂",fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:"🤣",fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:"🥳",fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:"😃",fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:"😄",fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:"😅",fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:"😆",fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:"😇",fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:"😉",fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:"😊",fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:"🙂",fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:"🙃",fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:"☺️",fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:"😋",fitzpatrick_scale:false,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:"😌",fitzpatrick_scale:false,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:"😍",fitzpatrick_scale:false,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:"🥰",fitzpatrick_scale:false,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😘",fitzpatrick_scale:false,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:"😗",fitzpatrick_scale:false,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:"😙",fitzpatrick_scale:false,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😚",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:"😜",fitzpatrick_scale:false,category:"people"},zany:{keywords:["face","goofy","crazy"],char:"🤪",fitzpatrick_scale:false,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:"🤨",fitzpatrick_scale:false,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:"🧐",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:"😝",fitzpatrick_scale:false,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:"😛",fitzpatrick_scale:false,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:"🤑",fitzpatrick_scale:false,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:"🤓",fitzpatrick_scale:false,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:"😎",fitzpatrick_scale:false,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:"🤩",fitzpatrick_scale:false,category:"people"},clown_face:{keywords:["face"],char:"🤡",fitzpatrick_scale:false,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:"🤠",fitzpatrick_scale:false,category:"people"},hugs:{keywords:["face","smile","hug"],char:"🤗",fitzpatrick_scale:false,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:"😏",fitzpatrick_scale:false,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:"😶",fitzpatrick_scale:false,category:"people"},neutral_face:{keywords:["indiffLine truncated
@@ -1,2 +0,0 @@
// Source: npm package: emojilib, file:emojis.json
window.tinymce.Resource.add("tinymce.plugins.emoticons",{grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:"\u{1f600}",fitzpatrick_scale:!1,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:"\u{1f62c}",fitzpatrick_scale:!1,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:"\u{1f601}",fitzpatrick_scale:!1,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:"\u{1f602}",fitzpatrick_scale:!1,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:"\u{1f923}",fitzpatrick_scale:!1,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:"\u{1f973}",fitzpatrick_scale:!1,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:"\u{1f603}",fitzpatrick_scale:!1,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:"\u{1f604}",fitzpatrick_scale:!1,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:"\u{1f605}",fitzpatrick_scale:!1,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:"\u{1f606}",fitzpatrick_scale:!1,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:"\u{1f607}",fitzpatrick_scale:!1,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:"\u{1f609}",fitzpatrick_scale:!1,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:"\u{1f60a}",fitzpatrick_scale:!1,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:"\u{1f642}",fitzpatrick_scale:!1,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:"\u{1f643}",fitzpatrick_scale:!1,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:"\u263a\ufe0f",fitzpatrick_scale:!1,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:"\u{1f60b}",fitzpatrick_scale:!1,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:"\u{1f60c}",fitzpatrick_scale:!1,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:"\u{1f60d}",fitzpatrick_scale:!1,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:"\u{1f970}",fitzpatrick_scale:!1,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"\u{1f618}",fitzpatrick_scale:!1,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:"\u{1f617}",fitzpatrick_scale:!1,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:"\u{1f619}",fitzpatrick_scale:!1,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"\u{1f61a}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:"\u{1f61c}",fitzpatrick_scale:!1,category:"people"},zany:{keywords:["face","goofy","crazy"],char:"\u{1f92a}",fitzpatrick_scale:!1,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:"\u{1f928}",fitzpatrick_scale:!1,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:"\u{1f9d0}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:"\u{1f61d}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:"\u{1f61b}",fitzpatrick_scale:!1,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:"\u{1f911}",fitzpatrick_scale:!1,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:"\u{1f913}",fitzpatrick_scale:!1,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:"\u{1f60e}",fitzpatrick_scale:!1,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:"\u{1f929}",fitzpatrick_scale:!1,category:"people"},clown_face:{keywords:["face"],char:"\u{1f921}",fitzpatrick_scale:!1,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:"\u{1f920}",fitzpatrick_scale:!1,category:"people"},hugs:{keywords:["face","smile","hug"],char:"\u{1f917}",fitzpatrick_scale:!1,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:"\u{1f60f}",fitzpatrick_scale:!1,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:"\u{Line truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>t===e,o=e(null),n=e(void 0),s=()=>{},r=()=>!1;class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return null==t?a.none():a.some(t)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const i=(t,e)=>{const o=t.length,n=new Array(o);for(let s=0;s<o;s++){const o=t[s];n[s]=e(o,s)}return n},l=t=>{let e=t;return{get:()=>e,set:t=>{e=t}}},c=Object.keys,u=Object.hasOwnProperty,g=(t,e)=>{const o=c(t);for(let n=0,s=o.length;n<s;n++){const s=o[n];e(t[s],s)}},d=(t,e)=>u.call(t,e),m=(h=(t,e)=>e,(...t)=>{if(0===t.length)throw new Error("Can't merge zero objects");const e={};for(let o=0;o<t.length;o++){const n=t[o];for(const t in n)d(n,t)&&(e[t]=h(e[t],n[t]))}return e});var h;const p=()=>{const t=(t=>{const e=l(a.none()),o=()=>e.get().each(t);return{clear:()=>{o(),e.set(a.none())},isSet:()=>e.get().isSome(),get:()=>e.get(),set:t=>{o(),e.set(a.some(t))}}})(s);return{...t,on:e=>t.get().each(e)}},y=(t,e,o=0,s)=>{const r=t.indexOf(e,o);return-1!==r&&(!!n(s)||r+e.length<=s)};var v=tinymce.util.Tools.resolve("tinymce.Resource");const f=t=>e=>e.options.get(t),b=f("emoticons_database"),w=f("emoticons_database_url"),j=f("emoticons_database_id"),C=f("emoticons_append"),_=f("emoticons_images_url"),A="All",k={symbols:"Symbols",people:"People",animals_and_nature:"Animals and Nature",food_and_drink:"Food and Drink",activity:"Activity",travel_and_places:"Travel and Places",objects:"Objects",flags:"Flags",user:"User Defined"},O=(t,e)=>d(t,e)?t[e]:e,x=t=>{const e=C(t);return o=t=>({keywords:[],category:"user",...t}),((t,e)=>{const o={};return g(t,((t,n)=>{const s=e(t,n);o[s.k]=s.v})),o})(e,((t,e)=>({k:e,v:o(t)})));var o},E=(t,e)=>y(t.title.toLowerCase(),e)||((t,o)=>{for(let o=0,s=t.length;o<s;o++)if(n=t[o],y(n.toLowerCase(),e))return!0;var n;return!1})(t.keywords),L=(t,e,o)=>{const n=[],s=e.toLowerCase(),a=o.fold((()=>r),(t=>e=>e>=t));for(let o=0;o<t.length&&(0!==e.length&&!E(t[o],s)||(n.push({value:t[o].char,text:t[o].title,icon:t[o].char}),!a(n.length)));o++);return n},S="pattern",N=(t,e)=>{const n={pattern:"",results:L(e.listAll(),"",a.some(300))},s=l(A),r=((t,e)=>{let n=null;const s=()=>{o(n)||(clearTimeout(n),n=null)};return{cancel:s,throttle:(...e)=>{s(),n=setTimeout((()=>{n=null,t.apply(null,e)}),200)}}})((t=>{(t=>{const o=t.getData(),n=s.get(),r=e.listCategory(n),i=L(r,o[S],n===A?a.some(300):a.none());t.setData({results:i})})(t)})),c={label:"Search",type:"input",name:S},u={type:"collection",name:"results"},g=()=>({title:"Emojis",size:"normal",body:{type:"tabpanel",tabs:i(e.listCategories(),(t=>({title:t,name:t,items:[c,u]})))},initialData:n,onTabChange:(t,e)=>{s.set(e.newTabName),r.throttle(t)},onChange:r.throttle,onAction:(e,o)=>{"results"===o.name&&(((t,e)=>{t.insertContent(e)})(t,o.value),e.close())},buttons:[{type:"cancel",text:"Close",primary:!0}]}),d=t.windowManager.open(g());d.focus(S),e.hasLoaded()||(d.block("Loading emojis..."),e.waitForLoad().then((()=>{d.redial(g()),r.throttle(d),d.focus(S),d.unblock()})).catch((t=>{d.redial({title:"Emojis",body:{type:"panel",items:[{type:"alertbanner",level:"error",icon:"warning",text:"Could not load emojis"}]},buttons:[{type:"cancel",text:"Close",primary:!0}],initialData:{pattern:"",results:[]}}),d.focus(S),d.unblock()})))},T=t=>e=>{const o=()=>{e.setEnabled(t.selection.isEditable())};return t.on("NodeChange",o),o(),()=>{t.off("NodeChange",o)}};t.add("emoticons",((t,e)=>{((t,e)=>{const o=t.options.register;o("emoticons_database",{processor:"string",default:"emojis"}),o("emoticons_database_url",{processor:"string",default:`${e}/js/${b(t)}${t.suffix}.js`}),o("emoticons_database_id",{processor:"string",default:"tinymce.plugins.emoticons"}),o("emoticons_append",{processor:"object",default:{}}),o("emoticons_images_url",{processor:"string",default:"https://cdnjs.cloudflare.com/ajax/libs/twemoji/15.1.0/72x72/"})})(t,e);const o=((t,e,o)=>{const n=p(),s=p(),r=_(t),i=t=>{return o="<img",(e=t.char).length>=4&&e.substr(0,4)===o?t.char.replace(/src="([^"]+)"/,((t,e)=>`src="${r}${e}"`)):t.char;var e,o};t.on("init",(()=>{v.load(o,e).then((e=>{const o=x(t);(t=>{const e={},o=[]Line truncated
@@ -1,4 +0,0 @@
/**
* TinyMCE version 7.1.1 (2024-05-22)
*/
!function(){"use strict";const e=e=>{let t=e;return{get:()=>t,set:e=>{t=e}}};var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const n=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=r=e,(o=String).prototype.isPrototypeOf(n)||(null===(s=r.constructor)||void 0===s?void 0:s.name)===o.name)?"string":t;var n,r,o,s})(t)===e,r=e=>t=>typeof t===e,o=e=>t=>e===t,s=n("string"),i=n("object"),l=n("array"),a=o(null),c=r("boolean"),u=o(void 0),d=e=>!(e=>null==e)(e),m=r("function"),h=r("number"),g=()=>{},p=e=>()=>e;function f(e,...t){return(...n)=>{const r=t.concat(n);return e.apply(null,r)}}const v=p(!1),w=p(!0);class y{constructor(e,t){this.tag=e,this.value=t}static some(e){return new y(!0,e)}static none(){return y.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?y.some(e(this.value)):y.none()}bind(e){return this.tag?e(this.value):y.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:y.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return d(e)?y.some(e):y.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}y.singletonNone=new y(!1);const b=Array.prototype.push,S=(e,t)=>{const n=e.length,r=new Array(n);for(let o=0;o<n;o++){const n=e[o];r[o]=t(n,o)}return r},x=(e,t)=>{for(let n=0,r=e.length;n<r;n++)t(e[n],n)},E=(e,t)=>{const n=[];for(let r=0,o=e.length;r<o;r++){const o=e[r];t(o,r)&&n.push(o)}return n},F=(e,t)=>((e,t,n)=>{for(let r=0,o=e.length;r<o;r++){const o=e[r];if(t(o,r))return y.some(o);if(n(o,r))break}return y.none()})(e,t,v),O=t=>{const n=e(y.none()),r=()=>n.get().each(t);return{clear:()=>{r(),n.set(y.none())},isSet:()=>n.get().isSome(),get:()=>n.get(),set:e=>{r(),n.set(y.some(e))}}},k=()=>O((e=>e.unbind())),T=Object.keys,C="undefined"!=typeof window?window:Function("return this;")(),A=(e,t)=>((e,t)=>{let n=null!=t?t:C;for(let t=0;t<e.length&&null!=n;++t)n=n[e[t]];return n})(e.split("."),t),R=Object.getPrototypeOf,L=e=>{const t=A("ownerDocument.defaultView",e);return i(e)&&((e=>((e,t)=>{const n=((e,t)=>A(e,t))(e,t);if(null==n)throw new Error(e+" not available on this browser");return n})("HTMLElement",e))(t).prototype.isPrototypeOf(e)||/^HTML\w*Element$/.test(R(e).constructor.name))},M=e=>t=>(e=>e.dom.nodeType)(t)===e,P=M(1),D=M(3),N=M(9),H=M(11),V=(e,t)=>{const n=e.dom.getAttribute(t);return null===n?void 0:n},W=(e,t)=>{e.dom.removeAttribute(t)},q=(e,t,n=0,r)=>{const o=e.indexOf(t,n);return-1!==o&&(!!u(r)||o+t.length<=r)},B=e=>void 0!==e.style&&m(e.style.getPropertyValue),I=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},j=I,_=(e,t)=>{const n=e.dom;if(1!==n.nodeType)return!1;{const e=n;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},z=e=>j(e.dom.ownerDocument),K=e=>S(e.dom.childNodes,j),$=m(Element.prototype.attachShadow)&&m(Node.prototype.getRootNode),U=p($),X=$?e=>j(e.dom.getRootNode()):e=>N(e)?e:z(e),Y=e=>{const t=X(e);return H(n=t)&&d(n.dom.host)?y.some(t):y.none();var n},G=e=>j(e.dom.host),J=e=>{const t=D(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const n=t.ownerDocument;return Y(j(t)).fold((()=>n.body.contains(t)),(r=J,o=G,e=>r(o(e))));var r,o},Q=(e,t,n)=>{if(!s(n))throw console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);B(e)&&e.style.setProperty(t,n)},Z=(e,t,n)=>{const r=e.dom;Q(r,t,n)},ee=(e,t)=>{const n=e.dom;((e,t)=>{const n=T(e);for(let r=0,o=n.length;r<o;r++){const o=n[r];t(e[o],o)}})(t,((e,t)=>{Q(n,t,e)}))},te=(e,t)=>{const n=e.dom,r=window.getComputedStyle(n).getPropertyValue(t);return""!==r||J(e)?r:ne(n,t)},ne=(e,t)=>B(e)?e.style.getPropertyValue(t):"",re=e=>{const t=j((e=>{if(U()&&d(e.target)){const t=j(e.target);if(P(t)&&d(t.dom.shadowRoot)&&e.composed&&e.composedPath){const t=e.composedPath();if(t)return((e,t)=>0<e.length?y.some(e[0]):y.none())(t)}}return y.from(e.target)})(e).getOr(e.target)),n=()=>e.stopPropagation(),r=()=>e.preventDefault(),o=(s=r,i=n,(...e)=>s(i.apply(null,e)));var s,i;return((e,t,n,r,o,s,i)=>({target:e,x:t,y:n,stop:r,prevent:o,kill:s,raw:i}))(t,e.clientX,e.clientY,n,r,o,e)},oe=(e,t,n,r)=>{e.dom.removeEventListener(t,n,r)},se=w,ie=(e,t,n)=>((e,t,n,r)=>((e,t,n,r,o)=>{const s=((e,t)=>n=>{e(n)&&t(re(Line truncated
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.ar',
'<h1>بدء التنقل بواسطة لوحة المفاتيح</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>التركيز على شريط القوائم</dt>\n' +
' <dd>نظاما التشغيل Windows أو Linux: Alt + F9</dd>\n' +
' <dd>نظام التشغيل macOS: &#x2325;F9</dd>\n' +
' <dt>التركيز على شريط الأدوات</dt>\n' +
' <dd>نظاما التشغيل Windows أو Linux: Alt + F10</dd>\n' +
' <dd>نظام التشغيل macOS: &#x2325;F10</dd>\n' +
' <dt>التركيز على التذييل</dt>\n' +
' <dd>نظاما التشغيل Windows أو Linux: Alt + F11</dd>\n' +
' <dd>نظام التشغيل macOS: &#x2325;F11</dd>\n' +
' <dt>تركيز الإشعارات</dt>\n' +
' <dd>نظاما التشغيل Windows أو Linux: Alt + F12</dd>\n' +
' <dd>نظام التشغيل macOS: &#x2325;F12</dd>\n' +
' <dt>التركيز على شريط أدوات السياق</dt>\n' +
' <dd>أنظمة التشغيل Windows أو Linux أو macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>سيبدأ التنقل عند عنصر واجهة المستخدم الأول، والذي سيتم تمييزه أو تسطيره في حالة العنصر الأول في\n' +
' مسار عنصر التذييل.</p>\n' +
'\n' +
'<h1>التنقل بين أقسام واجهة المستخدم</h1>\n' +
'\n' +
'<p>للانتقال من أحد أقسام واجهة المستخدم إلى القسم التالي، اضغط على <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>للانتقال من أحد أقسام واجهة المستخدم إلى القسم السابق، اضغط على <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>ترتيب علامات <strong>Tab</strong> لأقسام واجهة المستخدم هذه هو:</p>\n' +
'\n' +
'<ol>\n' +
' <li>شريط القوائم</li>\n' +
' <li>كل مجموعة شريط الأدوات</li>\n' +
' <li>الشريط الجانبي</li>\n' +
' <li>مسار العنصر في التذييل</li>\n' +
' <li>زر تبديل عدد الكلمات في التذييل</li>\n' +
' <li>رابط إدراج العلامة التجارية في التذييل</li>\n' +
' <li>مؤشر تغيير حجم المحرر في التذييل</li>\n' +
'</ol>\n' +
'\n' +
'<p>إذا لم يكن قسم واجهة المستخدم موجودًا، فسيتم تخطيه.</p>\n' +
'\n' +
'<p>إذا كان التذييل يحتوي على التركيز على ‏‫التنقل بواسطة لوحة المفاتيح، ولا يوجد شريط جانبي مرئي، فإن الضغط على <strong>Shift+Tab</strong>\n' +
' ينقل التركيز إلى مجموعة شريط الأدوات الأولى، وليس الأخيرة.</p>\n' +
'\n' +
'<h1>التنقل بين أقسام واجهة المستخدم</h1>\n' +
'\n' +
'<p>للانتقال من أحد عناصر واجهة المستخدم إلى العنصر التالي، اضغط على مفتاح <strong>السهم</strong> المناسب.</p>\n' +
'\n' +
'<p>مفتاحا السهمين <strong>اليسار‎</strong> و<strong>اليمين‎</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>التنقل بين القوائم في شريط القوائم.</li>\n' +
' <li>فتح قائمة فرعية في القائمة.</li>\n' +
' <li>التنقل بين الأزرار في مجموعة شريط الأدوات.</li>\n' +
' <li>التنقل بين العناصر في مسار عنصر التذييل.</li>\n' +
'</ul>\n' +
'\n' +
'<p>مفتاحا السهمين <strong>لأسفل‎</strong> و<strong>لأعلى‎</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>التنقل بين عناصر القائمة في القائمة.</li>\n' +
' <li>التنقل بين العناصر في قائمة شريط الأدوات المنبثقة.</li>\n' +
'</ul>\n' +
'\n' +
'<p>دورة مفاتيح <strong>الأسهم‎</strong> داخل قسم واجهة المستخدم التي تم التركيز عليها.</p>\n' +
'\n' +
'<p>لإغلاق قائمة مفتوحة أو قائمة فرعية مفتوحة أو قائمة منبثقة مفتوحة، اضغط على مفتاح <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>إذا كان التركيز الحالي على "الجزء العلوي" من قسم معين لواجهة المستخدم، فإن الضغط على مفتاح <strong>Esc</strong> يؤدي أيضًا إلى الخروج\n' +
' من التنقل بواسطة لوحة المفاتيح بالكامل.</p>\n' +
'\n' +
'<h1>تنفيذ عنصر قائمة أو زر شريط أدوات</h1>\n' +
'\n' +
'<p>عندما يتم تمييز عنصر القائمة المطلوب أو زر شريط الأدوات، اضغط على زر <strong>Return</strong>، أو <strong>Enter</strong>،\n' +
' أو <strong>مفتاح المسافة</strong> لتنفيذ العنصر.</p>\n' +
'\n' +
'<h1>التنقل في مربعات الحوار غير المبوبة</h1>\n' +
'\n' +
'<p>في مربعات الحوار غير المبوبة، يتم التركيز على المكون التفاعلي الأول عند فتح مربع الحوار.</p>\n' +
'\n' +
'<p>التنقل بين مكونات الحوار التفاعلي بالضغط على زر <strong>Tab</strong> أو <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>التنقل في مربعات الحوار المبوبة</h1>\n' +
'\n' +
'<p>في مربعات الحوار المبوبة، يتم التركيز على الزر الأول في قائمة علامات التبويب عند فتح مربع الحوار.</p>\n' +
'\n' +
'<p>التنقل بين المكونات التفاعلية لعلامة التبويب لمربع الحوار هذه بالضغط على زر <strong>Tab</strong> أو\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>التبديل إلى علامة تبويب أخرى لمربع الحوار من خلال التركيز على قائمة علامة التبويب ثم الضغط على زر <strong>السهم</strong> المناسب\n' +
' مفتاح للتنقل بين علامات التبويب المتاحة.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.bg_BG',
'<h1>Начало на навигацията с клавиатурата</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Фокусиране върху лентата с менюта</dt>\n' +
' <dd>Windows или Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Фокусиране върху лентата с инструменти</dt>\n' +
' <dd>Windows или Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Фокусиране върху долния колонтитул</dt>\n' +
' <dd>Windows или Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Фокусиране на известието</dt>\n' +
' <dd>Windows или Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Фокусиране върху контекстуалната лента с инструменти</dt>\n' +
' <dd>Windows, Linux или macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Навигацията ще започне с първия елемент на ПИ, който ще бъде маркиран или подчертан в случая на първия елемент в\n' +
' пътя до елемента в долния колонтитул.</p>\n' +
'\n' +
'<h1>Навигиране между раздели на ПИ</h1>\n' +
'\n' +
'<p>За да преминете от един раздел на ПИ към следващия, натиснете <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>За да преминете от един раздел на ПИ към предишния, натиснете <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Редът за <strong>обхождане с табулация</strong> на тези раздели на ПИ е:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Лентата с менюта</li>\n' +
' <li>Всяка група на лентата с инструменти</li>\n' +
' <li>Страничната лента</li>\n' +
' <li>Пътят до елемента в долния колонтитул</li>\n' +
' <li>Бутонът за превключване на броя на думите в долния колонтитул</li>\n' +
' <li>Връзката за търговска марка в долния колонтитул</li>\n' +
' <li>Манипулаторът за преоразмеряване на редактора в долния колонтитул</li>\n' +
'</ol>\n' +
'\n' +
'<p>Ако някой раздел на ПИ липсва, той се пропуска.</p>\n' +
'\n' +
'<p>Ако долният колонтитул има фокус за навигация с клавиатурата и няма странична лента, натискането на <strong>Shift+Tab</strong>\n' +
' премества фокуса към първата група на лентата с инструменти, а не към последната.</p>\n' +
'\n' +
'<h1>Навигиране в разделите на ПИ</h1>\n' +
'\n' +
'<p>За да преминете от един елемент на ПИ към следващия, натиснете съответния клавиш със <strong>стрелка</strong>.</p>\n' +
'\n' +
'<p>С клавишите със стрелка <strong>наляво</strong> и <strong>надясно</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>се придвижвате между менютата в лентата с менюто;</li>\n' +
' <li>отваряте подменю в меню;</li>\n' +
' <li>се придвижвате между бутоните в група на лентата с инструменти;</li>\n' +
' <li>се придвижвате между елементи в пътя до елемент в долния колонтитул.</li>\n' +
'</ul>\n' +
'\n' +
'<p>С клавишите със стрелка <strong>надолу</strong> и <strong>нагоре</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>се придвижвате между елементите от менюто в дадено меню;</li>\n' +
' <li>се придвижвате между елементите в изскачащо меню на лентата с инструменти.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Клавишите със <strong>стрелки</strong> се придвижват в рамките на фокусирания раздел на ПИ.</p>\n' +
'\n' +
'<p>За да затворите отворено меню, подменю или изскачащо меню, натиснете клавиша <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>Ако текущият фокус е върху „горната част“ на конкретен раздел на ПИ, натискането на клавиша <strong>Esc</strong> също излиза\n' +
' напълно от навигацията с клавиатурата.</p>\n' +
'\n' +
'<h1>Изпълнение на елемент от менюто или бутон от лентата с инструменти</h1>\n' +
'\n' +
'<p>Когато желаният елемент от менюто или бутон от лентата с инструменти е маркиран, натиснете <strong>Return</strong>, <strong>Enter</strong>\n' +
' или <strong>клавиша за интервал</strong>, за да изпълните елемента.</p>\n' +
'\n' +
'<h1>Навигиране в диалогови прозорци без раздели</h1>\n' +
'\n' +
'<p>В диалоговите прозорци без раздели първият интерактивен компонент се фокусира, когато се отвори диалоговият прозорец.</p>\n' +
'\n' +
'<p>Навигирайте между интерактивните компоненти на диалоговия прозорец, като натиснете <strong>Tab</strong> или <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Навигиране в диалогови прозорци с раздели</h1>\n' +
'\n' +
'<p>В диалоговите прозорци с раздели първият бутон в менюто с раздели се фокусира, когато се отвори диалоговият прозорец.</p>\n' +
'\n' +
'<p>Навигирайте между интерактивните компоненти на този диалогов раздел, като натиснете <strong>Tab</strong> или\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Превключете към друг диалогов раздел, като фокусирате върху менюто с раздели и след това натиснете съответния клавиш със <strong>стрелка</strong>,\n' +
' за да преминете през наличните раздели.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.ca',
'<h1>Inici de la navegació amb el teclat</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Enfocar la barra de menús</dt>\n' +
' <dd>Windows o Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
" <dt>Enfocar la barra d'eines</dt>\n" +
' <dd>Windows o Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Enfocar el peu de pàgina</dt>\n' +
' <dd>Windows o Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Enfocar la notificació</dt>\n' +
' <dd>Windows o Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
" <dt>Enfocar una barra d'eines contextual</dt>\n" +
' <dd>Windows, Linux o macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
"<p>La navegació començarà en el primer element de la interfície d'usuari, que es ressaltarà o subratllarà per al primer element a\n" +
" la ruta de l'element de peu de pàgina.</p>\n" +
'\n' +
"<h1>Navegació entre seccions de la interfície d'usuari</h1>\n" +
'\n' +
"<p>Per desplaçar-vos des d'una secció de la interfície d'usuari a la següent, premeu la tecla <strong>Tab</strong>.</p>\n" +
'\n' +
"<p>Per desplaçar-vos des d'una secció de la interfície d'usuari a l'anterior, premeu les tecles <strong>Maj+Tab</strong>.</p>\n" +
'\n' +
"<p>L'ordre en prémer la tecla <strong>Tab</strong> d'aquestes secciones de la interfície d'usuari és:</p>\n" +
'\n' +
'<ol>\n' +
' <li>Barra de menús</li>\n' +
" <li>Cada grup de la barra d'eines</li>\n" +
' <li>Barra lateral</li>\n' +
" <li>Ruta de l'element del peu de pàgina</li>\n" +
' <li>Botó de commutació de recompte de paraules al peu de pàgina</li>\n' +
' <li>Enllaç de marca del peu de pàgina</li>\n' +
" <li>Control de canvi de mida de l'editor al peu de pàgina</li>\n" +
'</ol>\n' +
'\n' +
"<p>Si no hi ha una secció de la interfície d'usuari, s'ometrà.</p>\n" +
'\n' +
'<p>Si el peu de pàgina té el focus de navegació del teclat i no hi ha cap barra lateral visible, en prémer <strong>Maj+Tab</strong>\n' +
" el focus es mou al primer grup de la barra d'eines, no l'últim.</p>\n" +
'\n' +
"<h1>Navegació dins de les seccions de la interfície d'usuari</h1>\n" +
'\n' +
"<p>Per desplaçar-vos des d'un element de la interfície d'usuari al següent, premeu la tecla de <strong>Fletxa</strong> adequada.</p>\n" +
'\n' +
'<p>Les tecles de fletxa <strong>Esquerra</strong> i <strong>Dreta</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>us permeten desplaçar-vos entre menús de la barra de menús.</li>\n' +
' <li>obren un submenú en un menú.</li>\n' +
" <li>us permeten desplaçar-vos entre botons d'un grup de la barra d'eines.</li>\n" +
" <li>us permeten desplaçar-vos entre elements de la ruta d'elements del peu de pàgina.</li>\n" +
'</ul>\n' +
'\n' +
'<p>Les tecles de fletxa <strong>Avall</strong> i <strong>Amunt</strong></p>\n' +
'\n' +
'<ul>\n' +
" <li>us permeten desplaçar-vos entre elements de menú d'un menú.</li>\n" +
" <li>us permeten desplaçar-vos entre elements d'un menú emergent de la barra d'eines.</li>\n" +
'</ul>\n' +
'\n' +
"<p>Les tecles de <strong>Fletxa</strong> us permeten desplaçar-vos dins de la secció de la interfície d'usuari que té el focus.</p>\n" +
'\n' +
'<p>Per tancar un menú, un submenú o un menú emergent oberts, premeu la tecla <strong>Esc</strong>.</p>\n' +
'\n' +
"<p>Si el focus actual es troba a la ‘part superior’ d'una secció específica de la interfície d'usuari, en prémer la tecla <strong>Esc</strong> també es tanca\n" +
' completament la navegació amb el teclat.</p>\n' +
'\n' +
"<h1>Execució d'un element de menú o d'un botó de la barra d'eines</h1>\n" +
'\n' +
"<p>Quan l'element del menú o el botó de la barra d'eines que desitgeu estigui ressaltat, premeu <strong>Retorn</strong>, <strong>Intro</strong>\n" +
" o la <strong>barra d'espai</strong> per executar l'element.</p>\n" +
'\n' +
'<h1>Navegació per quadres de diàleg sense pestanyes</h1>\n' +
'\n' +
"<p>En els quadres de diàleg sense pestanyes, el primer component interactiu pren el focus quan s'obre el quadre diàleg.</p>\n" +
'\n' +
'<p>Premeu la tecla <strong>Tab</strong> o les tecles <strong>Maj+Tab</strong> per desplaçar-vos entre components interactius del quadre de diàleg.</p>\n' +
'\n' +
'<h1>Navegació per quadres de diàleg amb pestanyes</h1>\n' +
'\n' +
"<p>En els quadres de diàleg amb pestanyes, el primer botó del menú de la pestanya pren el focus quan s'obre el quadre diàleg.</p>\n" +
'\n' +
"<p>Per desplaçar-vos entre components interactius d'aquest quadre de diàleg, premeu la tecla <strong>Tab</strong> o\n" +
' les tecles <strong>Maj+Tab</strong>.</p>\n' +
'\n' +
"<p>Canvieu a la pestanya d'un altre quadre de diàleg, tot enfocant el menú de la pestanya, i després premeu la tecla <strong>Fletxa</strong> adequada\n" +
' per canviar entre les pestanyes disponibles.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.cs',
'<h1>Začínáme navigovat pomocí klávesnice</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Přejít na řádek nabídek</dt>\n' +
' <dd>Windows nebo Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Přejít na panel nástrojů</dt>\n' +
' <dd>Windows nebo Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Přejít na zápatí</dt>\n' +
' <dd>Windows nebo Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Přejít na oznámení</dt>\n' +
' <dd>Windows nebo Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Přejít na kontextový panel nástrojů</dt>\n' +
' <dd>Windows, Linux nebo macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Navigace začne u první položky uživatelského rozhraní, která bude zvýrazněna nebo v případě první položky\n' +
' cesty k prvku zápatí podtržena.</p>\n' +
'\n' +
'<h1>Navigace mezi oddíly uživatelského rozhraní</h1>\n' +
'\n' +
'<p>Stisknutím klávesy <strong>Tab</strong> se posunete z jednoho oddílu uživatelského rozhraní na další.</p>\n' +
'\n' +
'<p>Stisknutím kláves <strong>Shift+Tab</strong> se posunete z jednoho oddílu uživatelského rozhraní na předchozí.</p>\n' +
'\n' +
'<p>Pořadí přepínání mezi oddíly uživatelského rozhraní pomocí klávesy <strong>Tab</strong>:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Řádek nabídek</li>\n' +
' <li>Každá skupina panelu nástrojů</li>\n' +
' <li>Boční panel</li>\n' +
' <li>Cesta k prvku v zápatí.</li>\n' +
' <li>Tlačítko přepínače počtu slov v zápatí</li>\n' +
' <li>Odkaz na informace o značce v zápatí</li>\n' +
' <li>Úchyt pro změnu velikosti editoru v zápatí</li>\n' +
'</ol>\n' +
'\n' +
'<p>Pokud nějaký oddíl uživatelského rozhraní není přítomen, je přeskočen.</p>\n' +
'\n' +
'<p>Pokud je zápatí vybrané pro navigaci pomocí klávesnice a není zobrazen žádný boční panel, stisknutím kláves <strong>Shift+Tab</strong>\n' +
' přejdete na první skupinu panelu nástrojů, nikoli na poslední.</p>\n' +
'\n' +
'<h1>Navigace v rámci oddílů uživatelského rozhraní</h1>\n' +
'\n' +
'<p>Chcete-li se přesunout z jednoho prvku uživatelského rozhraní na další, stiskněte příslušnou klávesu s <strong>šipkou</strong>.</p>\n' +
'\n' +
'<p>Klávesy s šipkou <strong>vlevo</strong> a <strong>vpravo</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>umožňují přesun mezi nabídkami na řádku nabídek;</li>\n' +
' <li>otevírají podnabídku nabídky;</li>\n' +
' <li>umožňují přesun mezi tlačítky ve skupině panelu nástrojů;</li>\n' +
' <li>umožňují přesun mezi položkami cesty prvku v zápatí.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Klávesy se šipkou <strong>dolů</strong> a <strong>nahoru</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>umožňují přesun mezi položkami nabídky;</li>\n' +
' <li>umožňují přesun mezi položkami místní nabídky panelu nástrojů.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Šipky</strong> provádí přepínání v rámci vybraného oddílu uživatelského rozhraní.</p>\n' +
'\n' +
'<p>Chcete-li zavřít otevřenou nabídku, podnabídku nebo místní nabídku, stiskněte klávesu <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>Pokud je aktuálně vybrána horní část oddílu uživatelského rozhraní, stisknutím klávesy <strong>Esc</strong> zcela ukončíte také\n' +
' navigaci pomocí klávesnice.</p>\n' +
'\n' +
'<h1>Provedení příkazu položky nabídky nebo tlačítka panelu nástrojů</h1>\n' +
'\n' +
'<p>Pokud je zvýrazněna požadovaná položka nabídky nebo tlačítko panelu nástrojů, stisknutím klávesy <strong>Return</strong>, <strong>Enter</strong>\n' +
' nebo <strong>mezerníku</strong> provedete příslušný příkaz.</p>\n' +
'\n' +
'<h1>Navigace v dialogových oknech bez záložek</h1>\n' +
'\n' +
'<p>Při otevření dialogových oken bez záložek přejdete na první interaktivní komponentu.</p>\n' +
'\n' +
'<p>Přecházet mezi interaktivními komponentami dialogového okna můžete stisknutím klávesy <strong>Tab</strong> nebo kombinace <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Navigace v dialogových oknech se záložkami</h1>\n' +
'\n' +
'<p>Při otevření dialogových oken se záložkami přejdete na první tlačítko v nabídce záložek.</p>\n' +
'\n' +
'<p>Přecházet mezi interaktivními komponentami této záložky dialogového okna můžete stisknutím klávesy <strong>Tab</strong> nebo\n' +
' kombinace <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Chcete-li přepnout na další záložku dialogového okna, přejděte na nabídku záložek a poté můžete stisknutím požadované <strong>šipky</strong>\n' +
' přepínat mezi dostupnými záložkami.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.da',
'<h1>Start tastaturnavigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokuser på menulinjen</dt>\n' +
' <dd>Windows eller Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Fokuser på værktøjslinjen</dt>\n' +
' <dd>Windows eller Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Fokuser på sidefoden</dt>\n' +
' <dd>Windows eller Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Fokuser på meddelelsen</dt>\n' +
' <dd>Windows eller Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Fokuser på kontekstuel værktøjslinje</dt>\n' +
' <dd>Windows, Linux eller macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Navigationen starter ved det første UI-element, som fremhæves eller understreges hvad angår det første element i\n' +
' sidefodens sti til elementet.</p>\n' +
'\n' +
'<h1>Naviger mellem UI-sektioner</h1>\n' +
'\n' +
'<p>Gå fra én UI-sektion til den næste ved at trykke på <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>Gå fra én UI-sektion til den forrige ved at trykke på <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p><strong>Tab</strong>-rækkefølgen af disse UI-sektioner er:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Menulinje</li>\n' +
' <li>Hver værktøjsgruppe</li>\n' +
' <li>Sidepanel</li>\n' +
' <li>Sti til elementet i sidefoden</li>\n' +
' <li>Til/fra-knap for ordoptælling i sidefoden</li>\n' +
' <li>Brandinglink i sidefoden</li>\n' +
' <li>Tilpasningshåndtag for editor i sidefoden</li>\n' +
'</ol>\n' +
'\n' +
'<p>Hvis en UI-sektion ikke er til stede, springes den over.</p>\n' +
'\n' +
'<p>Hvis sidefoden har fokus til tastaturnavigation, og der ikke er noget synligt sidepanel, kan der trykkes på <strong>Shift+Tab</strong>\n' +
' for at flytte fokus til den første værktøjsgruppe, ikke den sidste.</p>\n' +
'\n' +
'<h1>Naviger inden for UI-sektioner</h1>\n' +
'\n' +
'<p>Gå fra ét UI-element til det næste ved at trykke på den relevante <strong>piletast</strong>.</p>\n' +
'\n' +
'<p><strong>Venstre</strong> og <strong>højre</strong> piletast</p>\n' +
'\n' +
'<ul>\n' +
' <li>flytter mellem menuerne i menulinjen.</li>\n' +
' <li>åbner en undermenu i en menu.</li>\n' +
' <li>flytter mellem knapperne i en værktøjsgruppe.</li>\n' +
' <li>flytter mellem elementer i sidefodens sti til elementet.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Pil <strong>ned</strong> og <strong>op</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>flytter mellem menupunkterne i en menu.</li>\n' +
' <li>flytter mellem punkterne i en genvejsmenu i værktøjslinjen.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Piletasterne</strong> kører rundt inden for UI-sektionen, der fokuseres på.</p>\n' +
'\n' +
'<p>For at lukke en åben menu, en åben undermenu eller en åben genvejsmenu trykkes der på <strong>Esc</strong>-tasten.</p>\n' +
'\n' +
"<p>Hvis det aktuelle fokus er i 'toppen' af en bestemt UI-sektion, vil tryk på <strong>Esc</strong>-tasten også afslutte\n" +
' tastaturnavigationen helt.</p>\n' +
'\n' +
'<h1>Udfør et menupunkt eller en værktøjslinjeknap</h1>\n' +
'\n' +
'<p>Når det ønskede menupunkt eller den ønskede værktøjslinjeknap er fremhævet, trykkes der på <strong>Retur</strong>, <strong>Enter</strong>\n' +
' eller <strong>mellemrumstasten</strong> for at udføre elementet.</p>\n' +
'\n' +
'<h1>Naviger i ikke-faneopdelte dialogbokse</h1>\n' +
'\n' +
'<p>I ikke-faneopdelte dialogbokse får den første interaktive komponent fokus, når dialogboksen åbnes.</p>\n' +
'\n' +
'<p>Naviger mellem interaktive dialogbokskomponenter ved at trykke på <strong>Tab</strong> eller <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Naviger i faneopdelte dialogbokse</h1>\n' +
'\n' +
'<p>I faneopdelte dialogbokse får den første knap i fanemenuen fokus, når dialogboksen åbnes.</p>\n' +
'\n' +
'<p>Naviger mellem interaktive komponenter i denne dialogboksfane ved at trykke på <strong>Tab</strong> eller\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Skift til en anden dialogboksfane ved at fokusere på fanemenuen og derefter trykke på den relevante <strong>piletast</strong>\n' +
' for at køre igennem de tilgængelige faner.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.de',
'<h1>Grundlagen der Tastaturnavigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokus auf Menüleiste</dt>\n' +
' <dd>Windows oder Linux: ALT+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Fokus auf Symbolleiste</dt>\n' +
' <dd>Windows oder Linux: ALT+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Fokus auf Fußzeile</dt>\n' +
' <dd>Windows oder Linux: ALT+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Benachrichtigung fokussieren</dt>\n' +
' <dd>Windows oder Linux: ALT+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Fokus auf kontextbezogene Symbolleiste</dt>\n' +
' <dd>Windows, Linux oder macOS: STRG+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Die Navigation beginnt beim ersten Benutzeroberflächenelement, welches hervorgehoben ist. Falls sich das erste Element im Pfad der Fußzeile befindet,\n' +
' ist es unterstrichen.</p>\n' +
'\n' +
'<h1>Zwischen Abschnitten der Benutzeroberfläche navigieren</h1>\n' +
'\n' +
'<p>Um von einem Abschnitt der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie <strong>TAB</strong>.</p>\n' +
'\n' +
'<p>Um von einem Abschnitt der Benutzeroberfläche zum vorherigen zu wechseln, drücken Sie <strong>UMSCHALT+TAB</strong>.</p>\n' +
'\n' +
'<p>Die Abschnitte der Benutzeroberfläche haben folgende <strong>TAB</strong>-Reihenfolge:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Menüleiste</li>\n' +
' <li>Einzelne Gruppen der Symbolleiste</li>\n' +
' <li>Randleiste</li>\n' +
' <li>Elementpfad in der Fußzeile</li>\n' +
' <li>Umschaltfläche „Wörter zählen“ in der Fußzeile</li>\n' +
' <li>Branding-Link in der Fußzeile</li>\n' +
' <li>Editor-Ziehpunkt zur Größenänderung in der Fußzeile</li>\n' +
'</ol>\n' +
'\n' +
'<p>Falls ein Abschnitt der Benutzeroberflächen nicht vorhanden ist, wird er übersprungen.</p>\n' +
'\n' +
'<p>Wenn in der Fußzeile die Tastaturnavigation fokussiert ist und keine Randleiste angezeigt wird, wechselt der Fokus durch Drücken von <strong>UMSCHALT+TAB</strong>\n' +
' zur ersten Gruppe der Symbolleiste, nicht zur letzten.</p>\n' +
'\n' +
'<h1>Innerhalb von Abschnitten der Benutzeroberfläche navigieren</h1>\n' +
'\n' +
'<p>Um von einem Element der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie die entsprechende <strong>Pfeiltaste</strong>.</p>\n' +
'\n' +
'<p>Die Pfeiltasten <strong>Links</strong> und <strong>Rechts</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>wechseln zwischen Menüs in der Menüleiste.</li>\n' +
' <li>öffnen das Untermenü eines Menüs.</li>\n' +
' <li>wechseln zwischen Schaltflächen in einer Gruppe der Symbolleiste.</li>\n' +
' <li>wechseln zwischen Elementen im Elementpfad der Fußzeile.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Die Pfeiltasten <strong>Abwärts</strong> und <strong>Aufwärts</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>wechseln zwischen Menüelementen in einem Menü.</li>\n' +
' <li>wechseln zwischen Elementen in einem Popupmenü der Symbolleiste.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Die <strong>Pfeiltasten</strong> rotieren innerhalb des fokussierten Abschnitts der Benutzeroberfläche.</p>\n' +
'\n' +
'<p>Um ein geöffnetes Menü, ein geöffnetes Untermenü oder ein geöffnetes Popupmenü zu schließen, drücken Sie die <strong>ESC</strong>-Taste.</p>\n' +
'\n' +
'<p>Wenn sich der aktuelle Fokus ganz oben in einem bestimmten Abschnitt der Benutzeroberfläche befindet, wird durch Drücken der <strong>ESC</strong>-Taste auch\n' +
' die Tastaturnavigation beendet.</p>\n' +
'\n' +
'<h1>Ein Menüelement oder eine Symbolleistenschaltfläche ausführen</h1>\n' +
'\n' +
'<p>Wenn das gewünschte Menüelement oder die gewünschte Symbolleistenschaltfläche hervorgehoben ist, drücken Sie <strong>Zurück</strong>, <strong>Eingabe</strong>\n' +
' oder die <strong>Leertaste</strong>, um das Element auszuführen.</p>\n' +
'\n' +
'<h1>In Dialogfeldern ohne Registerkarten navigieren</h1>\n' +
'\n' +
'<p>In Dialogfeldern ohne Registerkarten ist beim Öffnen eines Dialogfelds die erste interaktive Komponente fokussiert.</p>\n' +
'\n' +
'<p>Navigieren Sie zwischen den interaktiven Komponenten eines Dialogfelds, indem Sie <strong>TAB</strong> oder <strong>UMSCHALT+TAB</strong> drücken.</p>\n' +
'\n' +
'<h1>In Dialogfeldern mit Registerkarten navigieren</h1>\n' +
'\n' +
'<p>In Dialogfeldern mit Registerkarten ist beim Öffnen eines Dialogfelds die erste Schaltfläche eines Registerkartenmenüs fokussiert.</p>\n' +
'\n' +
'<p>Navigieren Sie zwischen den interaktiven Komponenten auf dieser Registerkarte des Dialogfelds, indem Sie <strong>TAB</strong> oder\n' +
' <strong>UMSCHALT+TAB</strong> drücken.</p>\n' +
'\n' +
'<p>Wechseln Sie zu einer anderen Registerkarte des Dialogfelds, indem Sie den Fokus auf das Registerkartenmenü legen und dann die entsprechende <strong>Pfeiltaste</strong>\n' +
' drücken, um durch die verfügbaren Registerkarten zu rotieren.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.el',
'<h1>Έναρξη πλοήγησης μέσω πληκτρολογίου</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Εστίαση στη γραμμή μενού</dt>\n' +
' <dd>Windows ή Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Εστίαση στη γραμμή εργαλείων</dt>\n' +
' <dd>Windows ή Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Εστίαση στο υποσέλιδο</dt>\n' +
' <dd>Windows ή Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Εστίαση στην ειδοποίηση</dt>\n' +
' <dd>Windows ή Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Εστίαση σε γραμμή εργαλείων βάσει περιεχομένου</dt>\n' +
' <dd>Windows, Linux ή macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Η πλοήγηση θα ξεκινήσει από το πρώτο στοιχείο περιβάλλοντος χρήστη, που θα επισημαίνεται ή θα είναι υπογραμμισμένο,\n' +
' όπως στην περίπτωση της διαδρομής του στοιχείου Υποσέλιδου.</p>\n' +
'\n' +
'<h1>Πλοήγηση μεταξύ ενοτήτων του περιβάλλοντος χρήστη</h1>\n' +
'\n' +
'<p>Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην επόμενη, πιέστε το πλήκτρο <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην προηγούμενη, πιέστε τα πλήκτρα <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Η σειρά <strong>Tab</strong> αυτών των ενοτήτων περιβάλλοντος χρήστη είναι η εξής:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Γραμμή μενού</li>\n' +
' <li>Κάθε ομάδα γραμμής εργαλείων</li>\n' +
' <li>Πλαϊνή γραμμή</li>\n' +
' <li>Διαδρομή στοιχείου στο υποσέλιδο</li>\n' +
' <li>Κουμπί εναλλαγής μέτρησης λέξεων στο υποσέλιδο</li>\n' +
' <li>Σύνδεσμος επωνυμίας στο υποσέλιδο</li>\n' +
' <li>Λαβή αλλαγής μεγέθους προγράμματος επεξεργασίας στο υποσέλιδο</li>\n' +
'</ol>\n' +
'\n' +
'<p>Εάν δεν εμφανίζεται ενότητα περιβάλλοντος χρήστη, παραλείπεται.</p>\n' +
'\n' +
'<p>Εάν η εστίαση πλοήγησης βρίσκεται στο πληκτρολόγιο και δεν υπάρχει εμφανής πλαϊνή γραμμή, εάν πιέσετε <strong>Shift+Tab</strong>\n' +
' η εστίαση μετακινείται στην πρώτη ομάδα γραμμής εργαλείων, όχι στην τελευταία.</p>\n' +
'\n' +
'<h1>Πλοήγηση εντός των ενοτήτων του περιβάλλοντος χρήστη</h1>\n' +
'\n' +
'<p>Για να μετακινηθείτε από ένα στοιχείο περιβάλλοντος χρήστη στο επόμενο, πιέστε το αντίστοιχο πλήκτρο <strong>βέλους</strong>.</p>\n' +
'\n' +
'<p>Με τα πλήκτρα <strong>αριστερού</strong> και <strong>δεξιού</strong> βέλους</p>\n' +
'\n' +
'<ul>\n' +
' <li>γίνεται μετακίνηση μεταξύ των μενού στη γραμμή μενού.</li>\n' +
' <li>ανοίγει ένα υπομενού σε ένα μενού.</li>\n' +
' <li>γίνεται μετακίνηση μεταξύ κουμπιών σε μια ομάδα γραμμής εργαλείων.</li>\n' +
' <li>γίνεται μετακίνηση μεταξύ στοιχείων στη διαδρομή στοιχείου στο υποσέλιδο.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Με τα πλήκτρα <strong>επάνω</strong> και <strong>κάτω</strong> βέλους</p>\n' +
'\n' +
'<ul>\n' +
' <li>γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα μενού.</li>\n' +
' <li>γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα αναδυόμενο μενού γραμμής εργαλείων.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Με τα πλήκτρα <strong>βέλους</strong> γίνεται κυκλική μετακίνηση εντός της εστιασμένης ενότητας περιβάλλοντος χρήστη.</p>\n' +
'\n' +
'<p>Για να κλείσετε ένα ανοιχτό μενού, ένα ανοιχτό υπομενού ή ένα ανοιχτό αναδυόμενο μενού, πιέστε το πλήκτρο <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>Εάν η τρέχουσα εστίαση βρίσκεται στην κορυφή μιας ενότητας περιβάλλοντος χρήστη, πιέζοντας το πλήκτρο <strong>Esc</strong>,\n' +
' γίνεται επίσης πλήρης έξοδος από την πλοήγηση μέσω πληκτρολογίου.</p>\n' +
'\n' +
'<h1>Εκτέλεση ενός στοιχείου μενού ή κουμπιού γραμμής εργαλείων</h1>\n' +
'\n' +
'<p>Όταν το επιθυμητό στοιχείο μενού ή κουμπί γραμμής εργαλείων είναι επισημασμένο, πιέστε τα πλήκτρα <strong>Return</strong>, <strong>Enter</strong>,\n' +
' ή το <strong>πλήκτρο διαστήματος</strong> για να εκτελέσετε το στοιχείο.</p>\n' +
'\n' +
'<h1>Πλοήγηση σε παράθυρα διαλόγου χωρίς καρτέλες</h1>\n' +
'\n' +
'<p>Σε παράθυρα διαλόγου χωρίς καρτέλες, το πρώτο αλληλεπιδραστικό στοιχείο λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.</p>\n' +
'\n' +
'<p>Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων παραθύρων διαλόγων πιέζοντας τα πλήκτρα <strong>Tab</strong> ή <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Πλοήγηση σε παράθυρα διαλόγου με καρτέλες</h1>\n' +
'\n' +
'<p>Σε παράθυρα διαλόγου με καρτέλες, το πρώτο κουμπί στο μενού καρτέλας λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.</p>\n' +
'\n' +
'<p>Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων αυτής της καρτέλα διαλόγου πιέζοντας τα πλήκτρα <strong>Tab</strong> ή\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Μπορείτε να κάνετε εναλλαγή σε άλλη καρτέλα του παραθύρου διαλόγου, μεταφέροντας την εστίαση στο μενού καρτέλας και πιέζοντας το κατάλληλο πλήκτρο <strong>βέλους</strong>\n' +
' για να μετακινηθείτε κυκλικά στις διαθέσιμες καρτέλες.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.en',
'<h1>Begin keyboard navigation</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Focus the Menu bar</dt>\n' +
' <dd>Windows or Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Focus the Toolbar</dt>\n' +
' <dd>Windows or Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Focus the footer</dt>\n' +
' <dd>Windows or Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Focus the notification</dt>\n' +
' <dd>Windows or Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Focus a contextual toolbar</dt>\n' +
' <dd>Windows, Linux or macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Navigation will start at the first UI item, which will be highlighted, or underlined in the case of the first item in\n' +
' the Footer element path.</p>\n' +
'\n' +
'<h1>Navigate between UI sections</h1>\n' +
'\n' +
'<p>To move from one UI section to the next, press <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>To move from one UI section to the previous, press <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>The <strong>Tab</strong> order of these UI sections is:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Menu bar</li>\n' +
' <li>Each toolbar group</li>\n' +
' <li>Sidebar</li>\n' +
' <li>Element path in the footer</li>\n' +
' <li>Word count toggle button in the footer</li>\n' +
' <li>Branding link in the footer</li>\n' +
' <li>Editor resize handle in the footer</li>\n' +
'</ol>\n' +
'\n' +
'<p>If a UI section is not present, it is skipped.</p>\n' +
'\n' +
'<p>If the footer has keyboard navigation focus, and there is no visible sidebar, pressing <strong>Shift+Tab</strong>\n' +
' moves focus to the first toolbar group, not the last.</p>\n' +
'\n' +
'<h1>Navigate within UI sections</h1>\n' +
'\n' +
'<p>To move from one UI element to the next, press the appropriate <strong>Arrow</strong> key.</p>\n' +
'\n' +
'<p>The <strong>Left</strong> and <strong>Right</strong> arrow keys</p>\n' +
'\n' +
'<ul>\n' +
' <li>move between menus in the menu bar.</li>\n' +
' <li>open a sub-menu in a menu.</li>\n' +
' <li>move between buttons in a toolbar group.</li>\n' +
' <li>move between items in the footer’s element path.</li>\n' +
'</ul>\n' +
'\n' +
'<p>The <strong>Down</strong> and <strong>Up</strong> arrow keys</p>\n' +
'\n' +
'<ul>\n' +
' <li>move between menu items in a menu.</li>\n' +
' <li>move between items in a toolbar pop-up menu.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Arrow</strong> keys cycle within the focused UI section.</p>\n' +
'\n' +
'<p>To close an open menu, an open sub-menu, or an open pop-up menu, press the <strong>Esc</strong> key.</p>\n' +
'\n' +
'<p>If the current focus is at the ‘top’ of a particular UI section, pressing the <strong>Esc</strong> key also exits\n' +
' keyboard navigation entirely.</p>\n' +
'\n' +
'<h1>Execute a menu item or toolbar button</h1>\n' +
'\n' +
'<p>When the desired menu item or toolbar button is highlighted, press <strong>Return</strong>, <strong>Enter</strong>,\n' +
' or the <strong>Space bar</strong> to execute the item.</p>\n' +
'\n' +
'<h1>Navigate non-tabbed dialogs</h1>\n' +
'\n' +
'<p>In non-tabbed dialogs, the first interactive component takes focus when the dialog opens.</p>\n' +
'\n' +
'<p>Navigate between interactive dialog components by pressing <strong>Tab</strong> or <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<h1>Navigate tabbed dialogs</h1>\n' +
'\n' +
'<p>In tabbed dialogs, the first button in the tab menu takes focus when the dialog opens.</p>\n' +
'\n' +
'<p>Navigate between interactive components of this dialog tab by pressing <strong>Tab</strong> or\n' +
' <strong>Shift+Tab</strong>.</p>\n' +
'\n' +
'<p>Switch to another dialog tab by giving the tab menu focus and then pressing the appropriate <strong>Arrow</strong>\n' +
' key to cycle through the available tabs.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.es',
'<h1>Iniciar la navegación con el teclado</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Enfocar la barra de menús</dt>\n' +
' <dd>Windows o Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Enfocar la barra de herramientas</dt>\n' +
' <dd>Windows o Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Enfocar el pie de página</dt>\n' +
' <dd>Windows o Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Enfocar la notificación</dt>\n' +
' <dd>Windows o Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Enfocar una barra de herramientas contextual</dt>\n' +
' <dd>Windows, Linux o macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>La navegación comenzará por el primer elemento de la interfaz de usuario (IU), de tal manera que se resaltará, o bien se subrayará si se trata del primer elemento de\n' +
' la ruta de elemento del pie de página.</p>\n' +
'\n' +
'<h1>Navegar entre las secciones de la IU</h1>\n' +
'\n' +
'<p>Para pasar de una sección de la IU a la siguiente, pulse la tecla <strong>Tab</strong>.</p>\n' +
'\n' +
'<p>Para pasar de una sección de la IU a la anterior, pulse <strong>Mayús+Tab</strong>.</p>\n' +
'\n' +
'<p>El orden de <strong>tabulación</strong> de estas secciones de la IU es:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Barra de menús</li>\n' +
' <li>Cada grupo de barra de herramientas</li>\n' +
' <li>Barra lateral</li>\n' +
' <li>Ruta del elemento en el pie de página</li>\n' +
' <li>Botón de alternancia de recuento de palabras en el pie de página</li>\n' +
' <li>Enlace de personalización de marca en el pie de página</li>\n' +
' <li>Controlador de cambio de tamaño en el pie de página</li>\n' +
'</ol>\n' +
'\n' +
'<p>Si una sección de la IU no está presente, esta se omite.</p>\n' +
'\n' +
'<p>Si el pie de página tiene un enfoque de navegación con el teclado y no hay ninguna barra lateral visible, al pulsar <strong>Mayús+Tab</strong>,\n' +
' el enfoque se moverá al primer grupo de barra de herramientas, en lugar de al último.</p>\n' +
'\n' +
'<h1>Navegar dentro de las secciones de la IU</h1>\n' +
'\n' +
'<p>Para pasar de un elemento de la IU al siguiente, pulse la tecla de <strong>flecha</strong> correspondiente.</p>\n' +
'\n' +
'<p>Las teclas de flecha <strong>izquierda</strong> y <strong>derecha</strong> permiten</p>\n' +
'\n' +
'<ul>\n' +
' <li>desplazarse entre los menús de la barra de menús.</li>\n' +
' <li>abrir el submenú de un menú.</li>\n' +
' <li>desplazarse entre los botones de un grupo de barra de herramientas.</li>\n' +
' <li>desplazarse entre los elementos de la ruta de elemento del pie de página.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Las teclas de flecha <strong>abajo</strong> y <strong>arriba</strong> permiten</p>\n' +
'\n' +
'<ul>\n' +
' <li>desplazarse entre los elementos de menú de un menú.</li>\n' +
' <li>desplazarse entre los elementos de un menú emergente de una barra de herramientas.</li>\n' +
'</ul>\n' +
'\n' +
'<p>Las teclas de <strong>flecha</strong> van cambiando dentro de la sección de la IU enfocada.</p>\n' +
'\n' +
'<p>Para cerrar un menú, un submenú o un menú emergente que estén abiertos, pulse la tecla <strong>Esc</strong>.</p>\n' +
'\n' +
'<p>Si el enfoque actual se encuentra en la parte superior de una sección de la IU determinada, al pulsar la tecla <strong>Esc</strong> saldrá\n' +
' de la navegación con el teclado por completo.</p>\n' +
'\n' +
'<h1>Ejecutar un elemento de menú o un botón de barra de herramientas</h1>\n' +
'\n' +
'<p>Si el elemento de menú o el botón de barra de herramientas deseado está resaltado, pulse la tecla <strong>Retorno</strong> o <strong>Entrar</strong>,\n' +
' o la <strong>barra espaciadora</strong> para ejecutar el elemento.</p>\n' +
'\n' +
'<h1>Navegar por cuadros de diálogo sin pestañas</h1>\n' +
'\n' +
'<p>En los cuadros de diálogo sin pestañas, el primer componente interactivo se enfoca al abrirse el cuadro de diálogo.</p>\n' +
'\n' +
'<p>Para navegar entre los componentes interactivos del cuadro de diálogo, pulse las teclas <strong>Tab</strong> o <strong>Mayús+Tab</strong>.</p>\n' +
'\n' +
'<h1>Navegar por cuadros de diálogo con pestañas</h1>\n' +
'\n' +
'<p>En los cuadros de diálogo con pestañas, el primer botón del menú de pestaña se enfoca al abrirse el cuadro de diálogo.</p>\n' +
'\n' +
'<p>Para navegar entre componentes interactivos de esta pestaña del cuadro de diálogo, pulse las teclas <strong>Tab</strong> o\n' +
' <strong>Mayús+Tab</strong>.</p>\n' +
'\n' +
'<p>Si desea cambiar a otra pestaña del cuadro de diálogo, enfoque el menú de pestañas y, a continuación, pulse la tecla de <strong>flecha</strong>\n' +
' correspondiente para moverse por las pestañas disponibles.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.eu',
'<h1>Hasi teklatuaren nabigazioa</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Fokuratu menu-barra</dt>\n' +
' <dd>Windows edo Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Fokuratu tresna-barra</dt>\n' +
' <dd>Windows edo Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Fokuratu orri-oina</dt>\n' +
' <dd>Windows edo Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Fokuratu jakinarazpena</dt>\n' +
' <dd>Windows edo Linux: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Fokuratu testuinguruaren tresna-barra</dt>\n' +
' <dd>Windows, Linux edo macOS: Ktrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Nabigazioa EIko lehen elementuan hasiko da: elementu hori nabarmendu egingo da, edo azpimarratu lehen elementua bada\n' +
' orri-oineko elementuaren bidea.</p>\n' +
'\n' +
'<h1>Nabigatu EIko atalen artean</h1>\n' +
'\n' +
'<p>EIko atal batetik hurrengora mugitzeko, sakatu <strong>Tabuladorea</strong>.</p>\n' +
'\n' +
'<p>EIko atal batetik aurrekora mugitzeko, sakatu <strong>Maius+Tabuladorea</strong>.</p>\n' +
'\n' +
'<p>EIko atal hauen <strong>Tabuladorea</strong> da:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Menu-barra</li>\n' +
' <li>Tresna-barraren talde bakoitza</li>\n' +
' <li>Alboko barra</li>\n' +
' <li>Orri-oineko elementuaren bidea</li>\n' +
' <li>Orri-oneko urrats-kontaketa txandakatzeko botoia</li>\n' +
' <li>Orri-oineko marken esteka</li>\n' +
' <li>Orri-oineko editorearen tamaina aldatzeko heldulekua</li>\n' +
'</ol>\n' +
'\n' +
'<p>EIko atal bat ez badago, saltatu egin da.</p>\n' +
'\n' +
'<p>Orri-oinak teklatuaren nabigazioa fokuratuta badago, eta alboko barra ikusgai ez badago, <strong>Maius+Tabuladorea</strong> sakatuz gero,\n' +
' fokua tresna-barrako lehen taldera eramaten da, ez azkenera.</p>\n' +
'\n' +
'<h1>Nabigatu EIko atalen barruan</h1>\n' +
'\n' +
'<p>EIko elementu batetik hurrengora mugitzeko, sakatu dagokion <strong>Gezia</strong> tekla.</p>\n' +
'\n' +
'<p><strong>Ezkerrera</strong> eta <strong>Eskuinera</strong> gezi-teklak</p>\n' +
'\n' +
'<ul>\n' +
' <li>menu-barrako menuen artean mugitzen da.</li>\n' +
' <li>ireki azpimenu bat menuan.</li>\n' +
' <li>mugitu botoi batetik bestera tresna-barren talde batean.</li>\n' +
' <li>mugitu orri-oineko elementuaren bideko elementu batetik bestera.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Gora</strong> eta <strong>Behera</strong> gezi-teklak</p>\n' +
'\n' +
'<ul>\n' +
' <li>mugitu menu bateko menu-elementuen artean.</li>\n' +
' <li>mugitu tresna-barrako menu gainerakor bateko menu-elementuen artean.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Gezia</strong> teklen zikloa nabarmendutako EI atalen barruan.</p>\n' +
'\n' +
'<p>Irekitako menu bat ixteko, ireki azpimenua, edo ireki menu gainerakorra, sakatu <strong>Ihes</strong> tekla.</p>\n' +
'\n' +
'<p>Une horretan fokuratzea EIko atal jakin baten "goialdean" badago, <strong>Ihes</strong> tekla sakatuz gero\n' +
' teklatuaren nabigaziotik irtengo zara.</p>\n' +
'\n' +
'<h1>Exekutatu menuko elementu bat edo tresna-barrako botoi bat</h1>\n' +
'\n' +
'<p>Nahi den menuaren elementua edo tresna-barraren botoia nabarmenduta dagoenean, sakatu <strong>Itzuli</strong>, <strong>Sartu</strong>\n' +
' edo <strong>Zuriune-barra</strong> elementua exekutatzeko.</p>\n' +
'\n' +
'<h1>Nabigatu fitxarik gabeko elkarrizketak</h1>\n' +
'\n' +
'<p>Fitxarik gabeko elkarrizketetan, lehen osagai interaktiboa fokuratzen da elkarrizketa irekitzen denean.</p>\n' +
'\n' +
'<p>Nabigatu elkarrizketa interaktiboko osagai batetik bestera <strong>Tabuladorea</strong> edo <strong>Maius+Tabuladorea</strong> sakatuta.</p>\n' +
'\n' +
'<h1>Nabigatu fitxadun elkarrizketak</h1>\n' +
'\n' +
'<p>Fitxadun elkarrizketetan, fitxa-menuko lehen botoia fokuratzen da elkarrizketa irekitzen denean.</p>\n' +
'\n' +
'<p>Nabigatu elkarrizketa-fitxa honen interaktiboko osagai batetik bestera <strong>Tabuladorea</strong> edo\n' +
' <strong>Maius+Tabuladorea</strong> sakatuta.</p>\n' +
'\n' +
'<p>Aldatu beste elkarrizketa-fitxa batera fitxa-menua fokuratu eta dagokion <strong>Gezia</strong>\n' +
' tekla sakatzeko, erabilgarri dauden fitxa batetik bestera txandakatzeko.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fa',
'<h1>شروع پیمایش صفحه‌کلید</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>تمرکز بر نوار منو</dt>\n' +
' <dd>Windows یا Linux:‎‏: Alt+F9</dd>\n' +
' <dd>‎‏macOS: &#x2325;F9‎‏</dd>\n' +
' <dt>تمرکز بر نوار ابزار</dt>\n' +
' <dd>Windows یا Linux‎‏: Alt+F10</dd>\n' +
' <dd>‎‏macOS: &#x2325;F10‎‏</dd>\n' +
' <dt>تمرکز بر پانویس</dt>\n' +
' <dd>Windows یا Linux‎‏: Alt+F11</dd>\n' +
' <dd>‎‏macOS: &#x2325;F11‎‏</dd>\n' +
' <dt>تمرکز اعلان</dt>\n' +
' <dd>ویندوز یا لینوکس: Alt+F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>تمرکز بر نوار ابزار بافتاری</dt>\n' +
' <dd>Windows ،Linux یا macOS:‏ Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>پیمایش در اولین مورد رابط کاربری شروع می‌شود و درخصوص اولین مورد در\n' +
' مسیر عنصر پانویس، برجسته یا زیرخط‌دار می‌شود.</p>\n' +
'\n' +
'<h1>پیمایش بین بخش‌های رابط کاربری</h1>\n' +
'\n' +
'<p>برای جابجایی از یک بخش رابط کاربری به بخش بعدی، <strong>Tab</strong> را فشار دهید.</p>\n' +
'\n' +
'<p>برای جابجایی از یک بخش رابط کاربری به بخش قبلی، <strong>Shift+Tab</strong> را فشار دهید.</p>\n' +
'\n' +
'<p>ترتیب <strong>Tab</strong> این بخش‌های رابط کاربری عبارتند از:</p>\n' +
'\n' +
'<ol>\n' +
' <li>نوار منو</li>\n' +
' <li>هر گروه نوار ابزار</li>\n' +
' <li>نوار کناری</li>\n' +
' <li>مسیر عنصر در پانویس</li>\n' +
' <li>دکمه تغییر وضعیت تعداد کلمات در پانویس</li>\n' +
' <li>پیوند نمانام‌سازی در پانویس</li>\n' +
' <li>دسته تغییر اندازه ویرایشگر در پانویس</li>\n' +
'</ol>\n' +
'\n' +
'<p>اگر بخشی از رابط کاربری موجود نباشد، رد می‌شود.</p>\n' +
'\n' +
'<p>اگر پانویس دارای تمرکز بر پیمایش صفحه‌کلید باشد،‌ و نوار کناری قابل‌مشاهده وجود ندارد، فشردن <strong>Shift+Tab</strong>\n' +
' تمرکز را به گروه نوار ابزار اول می‌برد، نه آخر.</p>\n' +
'\n' +
'<h1>پیمایش در بخش‌های رابط کاربری</h1>\n' +
'\n' +
'<p>برای جابجایی از یک عنصر رابط کاربری به بعدی، کلید <strong>جهت‌نمای</strong> مناسب را فشار دهید.</p>\n' +
'\n' +
'<p>کلیدهای جهت‌نمای <strong>چپ</strong> و <strong>راست</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>جابجایی بین منوها در نوار منو.</li>\n' +
' <li>باز کردن منوی فرعی در یک منو.</li>\n' +
' <li>جابجایی بین دکمه‌ها در یک گروه نوار ابزار.</li>\n' +
' <li>جابجایی بین موارد در مسیر عنصر پانویس.</li>\n' +
'</ul>\n' +
'\n' +
'<p>کلیدهای جهت‌نمای <strong>پایین</strong> و <strong>بالا</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>جابجایی بین موارد منو در یک منو.</li>\n' +
' <li>جابجایی بین موارد در یک منوی بازشوی نوار ابزار.</li>\n' +
'</ul>\n' +
'\n' +
'<p>کلیدهای<strong>جهت‌نما</strong> در بخش رابط کاربری متمرکز می‌چرخند.</p>\n' +
'\n' +
'<p>برای بستن یک منوی باز، یک منوی فرعی باز، یا یک منوی بازشوی باز، کلید <strong>Esc</strong> را فشار دهید.</p>\n' +
'\n' +
'<p>اگر تمرکز فعلی در «بالای» یک بخش رابط کاربری خاص است، فشردن کلید <strong>Esc</strong> نیز موجب\n' +
' خروج کامل از پیمایش صفحه‌کلید می‌شود.</p>\n' +
'\n' +
'<h1>اجرای یک مورد منو یا دکمه نوار ابزار</h1>\n' +
'\n' +
'<p>وقتی مورد منو یا دکمه نوار ابزار مورد نظر هایلایت شد، دکمه <strong>بازگشت</strong>، <strong>Enter</strong>،\n' +
' یا <strong>نوار Space</strong> را فشار دهید تا مورد را اجرا کنید.</p>\n' +
'\n' +
'<h1>پیمایش در کادرهای گفتگوی بدون زبانه</h1>\n' +
'\n' +
'<p>در کادرهای گفتگوی بدون زبانه، وقتی کادر گفتگو باز می‌شود، اولین جزء تعاملی متمرکز می‌شود.</p>\n' +
'\n' +
'<p>با فشردن <strong>Tab</strong> یا <strong>Shift+Tab</strong>، بین اجزای کادر گفتگوی تعاملی پیمایش کنید.</p>\n' +
'\n' +
'<h1>پیمایش کادرهای گفتگوی زبانه‌دار</h1>\n' +
'\n' +
'<p>در کادرهای گفتگوی زبانه‌دار، وقتی کادر گفتگو باز می‌شود، اولین دکمه در منوی زبانه متمرکز می‌شود.</p>\n' +
'\n' +
'<p>با فشردن <strong>Tab</strong> یا\n' +
' <strong>Shift+Tab</strong>، بین اجزای تعاملی این زبانه کادر گفتگو پیمایش کنید.</p>\n' +
'\n' +
'<p>با دادن تمرکز به منوی زبانه و سپس فشار دادن کلید <strong>جهت‌نمای</strong>\n' +
' مناسب برای چرخش میان زبانه‌های موجود، به زبانه کادر گفتگوی دیگری بروید.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fi',
'<h1>Näppäimistönavigoinnin aloittaminen</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Siirrä kohdistus valikkopalkkiin</dt>\n' +
' <dd>Windows tai Linux: Alt+F9</dd>\n' +
' <dd>macOS: &#x2325;F9</dd>\n' +
' <dt>Siirrä kohdistus työkalupalkkiin</dt>\n' +
' <dd>Windows tai Linux: Alt+F10</dd>\n' +
' <dd>macOS: &#x2325;F10</dd>\n' +
' <dt>Siirrä kohdistus alatunnisteeseen</dt>\n' +
' <dd>Windows tai Linux: Alt+F11</dd>\n' +
' <dd>macOS: &#x2325;F11</dd>\n' +
' <dt>Keskitä ilmoitukseen</dt>\n' +
' <dd>Windows ja Linux: Alt + F12</dd>\n' +
' <dd>macOS: &#x2325;F12</dd>\n' +
' <dt>Siirrä kohdistus kontekstuaaliseen työkalupalkkiin</dt>\n' +
' <dd>Windows, Linux tai macOS: Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
'<p>Navigointi aloitetaan ensimmäisestä käyttöliittymän kohteesta, joka joko korostetaan tai alleviivataan, jos\n' +
' kyseessä on Alatunniste-elementin polun ensimmäinen kohde.</p>\n' +
'\n' +
'<h1>Käyttöliittymän eri osien välillä navigointi</h1>\n' +
'\n' +
'<p>Paina <strong>sarkainnäppäintä</strong> siirtyäksesi käyttöliittymän osasta seuraavaan.</p>\n' +
'\n' +
'<p>Jos haluat siirtyä edelliseen käyttöliittymän osaan, paina <strong>Shift+sarkainnäppäin</strong>.</p>\n' +
'\n' +
'<p><strong>Sarkainnäppäin</strong> siirtää sinua näissä käyttöliittymän osissa tässä järjestyksessä:</p>\n' +
'\n' +
'<ol>\n' +
' <li>Valikkopalkki</li>\n' +
' <li>Työkalupalkin ryhmät</li>\n' +
' <li>Sivupalkki</li>\n' +
' <li>Elementin polku alatunnisteessa</li>\n' +
' <li>Sanalaskurin vaihtopainike alatunnisteessa</li>\n' +
' <li>Brändäyslinkki alatunnisteessa</li>\n' +
' <li>Editorin koon muuttamisen kahva alatunnisteessa</li>\n' +
'</ol>\n' +
'\n' +
'<p>Jos jotakin käyttöliittymän osaa ei ole, se ohitetaan.</p>\n' +
'\n' +
'<p>Jos kohdistus on siirretty alatunnisteeseen näppäimistönavigoinnilla eikä sivupalkkia ole näkyvissä, <strong>Shift+sarkainnäppäin</strong>\n' +
' siirtää kohdistuksen työkalupalkin ensimmäiseen ryhmään, eikä viimeiseen.</p>\n' +
'\n' +
'<h1>Käyttöliittymän eri osien sisällä navigointi</h1>\n' +
'\n' +
'<p>Paina <strong>nuolinäppäimiä</strong> siirtyäksesi käyttöliittymäelementistä seuraavaan.</p>\n' +
'\n' +
'<p><strong>Vasen</strong>- ja <strong>Oikea</strong>-nuolinäppäimet</p>\n' +
'\n' +
'<ul>\n' +
' <li>siirtävät sinua valikkopalkin valikoiden välillä.</li>\n' +
' <li>avaavat valikon alavalikon.</li>\n' +
' <li>siirtävät sinua työkalupalkin ryhmän painikkeiden välillä.</li>\n' +
' <li>siirtävät sinua kohteiden välillä alatunnisteen elementin polussa.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Alas</strong>- ja <strong>Ylös</strong>-nuolinäppäimet</p>\n' +
'\n' +
'<ul>\n' +
' <li>siirtävät sinua valikon valikkokohteiden välillä.</li>\n' +
' <li>siirtävät sinua työkalupalkin ponnahdusvalikon kohteiden välillä.</li>\n' +
'</ul>\n' +
'\n' +
'<p><strong>Nuolinäppäimet</strong> siirtävät sinua käyttöliittymän korostetun osan sisällä syklissä.</p>\n' +
'\n' +
'<p>Paina <strong>Esc</strong>-näppäintä sulkeaksesi avoimen valikon, avataksesi alavalikon tai avataksesi ponnahdusvalikon.</p>\n' +
'\n' +
'<p>Jos kohdistus on käyttöliittymän tietyn osion ylälaidassa, <strong>Esc</strong>-näppäimen painaminen\n' +
' poistuu myös näppäimistönavigoinnista kokonaan.</p>\n' +
'\n' +
'<h1>Suorita valikkokohde tai työkalupalkin painike</h1>\n' +
'\n' +
'<p>Kun haluamasi valikkokohde tai työkalupalkin painike on korostettuna, paina <strong>Return</strong>-, <strong>Enter</strong>-\n' +
' tai <strong>välilyöntinäppäintä</strong> suorittaaksesi kohteen.</p>\n' +
'\n' +
'<h1>Välilehdittömissä valintaikkunoissa navigointi</h1>\n' +
'\n' +
'<p>Kun välilehdetön valintaikkuna avautuu, kohdistus siirtyy sen ensimmäiseen interaktiiviseen komponenttiin.</p>\n' +
'\n' +
'<p>Voit siirtyä valintaikkunan interaktiivisten komponenttien välillä painamalla <strong>sarkainnäppäintä</strong> tai <strong>Shift+sarkainnäppäin</strong>.</p>\n' +
'\n' +
'<h1>Välilehdellisissä valintaikkunoissa navigointi</h1>\n' +
'\n' +
'<p>Kun välilehdellinen valintaikkuna avautuu, kohdistus siirtyy välilehtivalikon ensimmäiseen painikkeeseen.</p>\n' +
'\n' +
'<p>Voit siirtyä valintaikkunan välilehden interaktiivisen komponenttien välillä painamalla <strong>sarkainnäppäintä</strong> tai\n' +
' <strong>Shift+sarkainnäppäin</strong>.</p>\n' +
'\n' +
'<p>Voit siirtyä valintaikkunan toiseen välilehteen siirtämällä kohdistuksen välilehtivalikkoon ja painamalla sopivaa <strong>nuolinäppäintä</strong>\n' +
' siirtyäksesi käytettävissä olevien välilehtien välillä syklissä.</p>\n');
@@ -1,93 +0,0 @@
tinymce.Resource.add('tinymce.html-i18n.help-keynav.fr_FR',
'<h1>Débuter la navigation au clavier</h1>\n' +
'\n' +
'<dl>\n' +
' <dt>Cibler la barre du menu</dt>\n' +
' <dd>Windows ou Linux : Alt+F9</dd>\n' +
' <dd>macOS : &#x2325;F9</dd>\n' +
" <dt>Cibler la barre d'outils</dt>\n" +
' <dd>Windows ou Linux : Alt+F10</dd>\n' +
' <dd>macOS : &#x2325;F10</dd>\n' +
' <dt>Cibler le pied de page</dt>\n' +
' <dd>Windows ou Linux : Alt+F11</dd>\n' +
' <dd>macOS : &#x2325;F11</dd>\n' +
' <dt>Cibler la notification</dt>\n' +
' <dd>Windows ou Linux : Alt+F12</dd>\n' +
' <dd>macOS : &#x2325;F12</dd>\n' +
" <dt>Cibler une barre d'outils contextuelle</dt>\n" +
' <dd>Windows, Linux ou macOS : Ctrl+F9</dd>\n' +
'</dl>\n' +
'\n' +
"<p>La navigation débutera sur le premier élément de l'interface utilisateur, qui sera mis en surbrillance ou bien souligné dans le cas du premier élément du\n" +
" chemin d'éléments du pied de page.</p>\n" +
'\n' +
"<h1>Naviguer entre les sections de l'interface utilisateur</h1>\n" +
'\n' +
"<p>Pour passer d'une section de l'interface utilisateur à la suivante, appuyez sur <strong>Tabulation</strong>.</p>\n" +
'\n' +
"<p>Pour passer d'une section de l'interface utilisateur à la précédente, appuyez sur <strong>Maj+Tabulation</strong>.</p>\n" +
'\n' +
"<p>L'ordre de <strong>Tabulation</strong> de ces sections de l'interface utilisateur est le suivant :</p>\n" +
'\n' +
'<ol>\n' +
' <li>Barre du menu</li>\n' +
" <li>Chaque groupe de barres d'outils</li>\n" +
' <li>Barre latérale</li>\n' +
" <li>Chemin d'éléments du pied de page</li>\n" +
" <li>Bouton d'activation du compteur de mots dans le pied de page</li>\n" +
' <li>Lien de marque dans le pied de page</li>\n' +
" <li>Poignée de redimensionnement de l'éditeur dans le pied de page</li>\n" +
'</ol>\n' +
'\n' +
"<p>Si une section de l'interface utilisateur n'est pas présente, elle sera ignorée.</p>\n" +
'\n' +
"<p>Si le pied de page comporte un ciblage par navigation au clavier et qu'il n'y a aucune barre latérale visible, appuyer sur <strong>Maj+Tabulation</strong>\n" +
" déplace le ciblage vers le premier groupe de barres d'outils et non le dernier.</p>\n" +
'\n' +
"<h1>Naviguer au sein des sections de l'interface utilisateur</h1>\n" +
'\n' +
"<p>Pour passer d'un élément de l'interface utilisateur au suivant, appuyez sur la <strong>Flèche</strong> appropriée.</p>\n" +
'\n' +
'<p>Les touches fléchées <strong>Gauche</strong> et <strong>Droite</strong></p>\n' +
'\n' +
'<ul>\n' +
' <li>se déplacent entre les menus de la barre des menus.</li>\n' +
" <li>ouvrent un sous-menu au sein d'un menu.</li>\n" +
" <li>se déplacent entre les boutons d'un groupe de barres d'outils.</li>\n" +
" <li>se déplacent entre les éléments du chemin d'éléments du pied de page.</li>\n" +
'</ul>\n' +
'\n' +
'<p>Les touches fléchées <strong>Bas</strong> et <strong>Haut</strong></p>\n' +
'\n' +
'<ul>\n' +
" <li>se déplacent entre les éléments de menu au sein d'un menu.</li>\n" +
" <li>se déplacent entre les éléments au sein d'un menu contextuel de barre d'outils.</li>\n" +
'</ul>\n' +
'\n' +
"<p>Les <strong>Flèches</strong> parcourent la section de l'interface utilisateur ciblée.</p>\n" +
'\n' +
'<p>Pour fermer un menu ouvert, un sous-menu ouvert ou un menu contextuel ouvert, appuyez sur <strong>Echap</strong>.</p>\n' +
'\n' +
"<p>Si l'actuel ciblage se trouve en « haut » d'une section spécifique de l'interface utilisateur, appuyer sur <strong>Echap</strong> permet également de quitter\n" +
' entièrement la navigation au clavier.</p>\n' +
'\n' +
"<h1>Exécuter un élément de menu ou un bouton de barre d'outils</h1>\n" +
'\n' +
"<p>Lorsque l'élément de menu ou le bouton de barre d'outils désiré est mis en surbrillance, appuyez sur la touche <strong>Retour arrière</strong>, <strong>Entrée</strong>\n" +
" ou la <strong>Barre d'espace</strong> pour exécuter l'élément.</p>\n" +
'\n' +
'<h1>Naviguer au sein de dialogues sans onglets</h1>\n' +
'\n' +
"<p>Dans les dialogues sans onglets, le premier composant interactif est ciblé lorsque le dialogue s'ouvre.</p>\n" +
'\n' +
'<p>Naviguez entre les composants du dialogue interactif en appuyant sur <strong>Tabulation</strong> ou <strong>Maj+Tabulation</strong>.</p>\n' +
'\n' +
'<h1>Naviguer au sein de dialogues avec onglets</h1>\n' +
'\n' +
"<p>Dans les dialogues avec onglets, le premier bouton du menu de l'onglet est ciblé lorsque le dialogue s'ouvre.</p>\n" +
'\n' +
'<p>Naviguez entre les composants interactifs de cet onglet de dialogue en appuyant sur <strong>Tabulation</strong> ou\n' +
' <strong>Maj+Tabulation</strong>.</p>\n' +
'\n' +
"<p>Passez à un autre onglet de dialogue en ciblant le menu de l'onglet et en appuyant sur la <strong>Flèche</strong>\n" +
' appropriée pour parcourir les onglets disponibles.</p>\n');
Loaded 100 of 418 files, more files were not shown because too many files have changed in this diff. Show more