Compare commits

...

14 Commits

Author SHA1 Message Date
Dan Brown
a9f5e98ba9 Drawings: Added class to extract drawio data from png files 2025-06-19 17:23:56 +01:00
Dan Brown
c4839c783a Updated translator & dependency attribution before release v25.05.1 2025-06-17 15:29:12 +01:00
Dan Brown
a5751a584c Updated translations with latest Crowdin changes (#5637) 2025-06-17 15:16:25 +01:00
Dan Brown
f518a3be37 Search: Updated indexer to handle non-breaking-spaces
Related to #5640
2025-06-17 14:00:13 +01:00
Dan Brown
0208f066c5 Comments: Fixed update notification text
For #5642
2025-06-17 13:42:25 +01:00
Dan Brown
2d0461b63a Merge pull request #5653 from BookStackApp/v25-05-1-lexical
Lexical Fixes for v25.05.1
2025-06-17 13:36:55 +01:00
Dan Brown
b913ae703d Lexical: Media form improvements
- Allowed re-editing of existing embed HTML code.
- Handled "src" form field when video is using child source tags.
2025-06-15 20:00:28 +01:00
Dan Brown
1611b0399f Lexical: Added a media toolbar, improved toolbars and media selection
- Updated toolbars to auto-refresh ui if it attempts to update targeting
  a DOM element which no longer exists.
- Removed MediaNode dom specific click handling which was causing
  selection issues, and did not seem to be needed now.
2025-06-15 15:22:27 +01:00
Dan Brown
8d4b8ff4f3 Lexical: Fixed media resize handling
- Updating height/width setting to clear any inline CSS width/height
  rules which would override and prevent resizes showing. This was
  common when switching media from old editor.
  Added test to cover.
- Updated resizer to track node so that it is retained & displayed
  across node DOM changes, which was previously causing the
  resizer/focus to disappear.
2025-06-15 13:55:42 +01:00
Dan Brown
77a88618c2 Lexical: Fixed double-bold text, updated tests
Double bold was due to text field exporting wrapping the output in <b>
tags when the main tag would already be strong.
2025-06-14 14:50:10 +01:00
Dan Brown
8b062d4795 Lexical: Fixed strange paragraph formatting behaviour
Formatting was not persisted on empty paragraphs, and was instead based
upon last format encountered in selection.
This was due to overly-hasty removal of other formatting code, which
this got caught it.
Restored required parts from prior codebase.

Also updated inline format button active indicator to reflect formats
using the above, so correct buttons are shown as active even when just
in an empty paragraph.
2025-06-13 19:40:13 +01:00
Dan Brown
717b516341 Lexical: Made table resize handles more efficent & less buggy
Fine mouse movement and handles will now only be active when actually
within a table, otherwise less frequent mouseovers are used to track if
in/out a table.
Hides handles when out of a table, preventing a range of issues with
stray handles floating about.
2025-06-13 16:38:53 +01:00
Dan Brown
fda242d3da Lexical: Fixed tiny image resizer on image insert
Added specific focus on image insert, and updated resize handler to
watch for load events and toggle a resize once loaded.
2025-06-13 15:58:59 +01:00
Dan Brown
aac547934c Deps: Bumped composer php package versions 2025-06-13 15:28:11 +01:00
55 changed files with 696 additions and 300 deletions

View File

@@ -487,3 +487,5 @@ jellium :: French
Qxlkdr :: Swedish
Hari (muhhari) :: Indonesian
仙君御 (xjy) :: Chinese Simplified
TapioM :: Finnish
lingb58 :: Chinese Traditional

View File

@@ -0,0 +1,7 @@
<?php
namespace BookStack\Exceptions;
class DrawioPngReaderException extends \Exception
{
}

View File

@@ -160,7 +160,9 @@ class SearchIndex
/** @var DOMNode $child */
foreach ($doc->getBodyChildren() as $child) {
$nodeName = $child->nodeName;
$termCounts = $this->textToTermCountMap(trim($child->textContent));
$text = trim($child->textContent);
$text = str_replace("\u{00A0}", ' ', $text);
$termCounts = $this->textToTermCountMap($text);
foreach ($termCounts as $term => $count) {
$scoreChange = $count * ($elementScoreAdjustmentMap[$nodeName] ?? 1);
$scoresByTerm[$term] = ($scoresByTerm[$term] ?? 0) + $scoreChange;

View File

@@ -0,0 +1,122 @@
<?php
namespace BookStack\Uploads;
use BookStack\Exceptions\DrawioPngReaderException;
/**
* Reads the PNG file format: https://www.w3.org/TR/2003/REC-PNG-20031110/
* So that it can extract embedded drawing data for alternative use.
*/
class DrawioPngReader
{
/**
* @param resource $fileStream
*/
public function __construct(
protected $fileStream
) {
}
/**
* @throws DrawioPngReaderException
*/
public function extractDrawing(): string
{
$signature = fread($this->fileStream, 8);
$pngSignature = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
if ($signature !== $pngSignature) {
throw new DrawioPngReaderException('File does not appear to be a valid PNG file');
}
$offset = 8;
$searching = true;
while ($searching) {
fseek($this->fileStream, $offset);
$lengthBytes = $this->readData(4);
$chunkTypeBytes = $this->readData(4);
$length = unpack('Nvalue', $lengthBytes)['value'];
if ($chunkTypeBytes === 'tEXt') {
fseek($this->fileStream, $offset + 8);
$data = $this->readData($length);
$crc = $this->readData(4);
$drawingData = $this->readTextForDrawing($data);
if ($drawingData !== null) {
$crcResult = $this->calculateCrc($chunkTypeBytes . $data);
if ($crc !== $crcResult) {
throw new DrawioPngReaderException('Drawing data withing PNG file appears to be corrupted');
}
return $drawingData;
}
} else if ($chunkTypeBytes === 'IEND') {
$searching = false;
}
$offset += 12 + $length; // 12 = length + type + crc bytes
}
throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
}
protected function readTextForDrawing(string $data): ?string
{
// Check the keyword is mxfile to ensure we're getting the right data
if (!str_starts_with($data, "mxfile\u{0}")) {
return null;
}
// Extract & cleanup the drawing text
$drawingText = substr($data, 7);
return urldecode($drawingText);
}
protected function readData(int $length): string
{
$bytes = fread($this->fileStream, $length);
if ($bytes === false || strlen($bytes) < $length) {
throw new DrawioPngReaderException('Unable to find drawing data within PNG file');
}
return $bytes;
}
protected function getCrcTable(): array
{
$table = [];
for ($n = 0; $n < 256; $n++) {
$c = $n;
for ($k = 0; $k < 8; $k++) {
if ($c & 1) {
$c = 0xedb88320 ^ ($c >> 1);
} else {
$c = $c >> 1;
}
}
$table[$n] = $c;
}
return $table;
}
/**
* Calculate a CRC for the given bytes following:
* https://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
*/
protected function calculateCrc(string $bytes): string
{
$table = $this->getCrcTable();
$length = strlen($bytes);
$c = 0xffffffff;
for ($n = 0; $n < $length; $n++) {
$tableIndex = ($c ^ ord($bytes[$n])) & 0xff;
$c = $table[$tableIndex] ^ ($c >> 8);
}
return pack('N', $c ^ 0xffffffff);
}
}

194
composer.lock generated
View File

@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.343.22",
"version": "3.344.6",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "174cc187df3bde52c21e9c00a4e99610a08732a3"
"reference": "eb0bc621472592545539329499961a15a3f9f6dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/174cc187df3bde52c21e9c00a4e99610a08732a3",
"reference": "174cc187df3bde52c21e9c00a4e99610a08732a3",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eb0bc621472592545539329499961a15a3f9f6dc",
"reference": "eb0bc621472592545539329499961a15a3f9f6dc",
"shasum": ""
},
"require": {
@@ -153,9 +153,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.343.22"
"source": "https://github.com/aws/aws-sdk-php/tree/3.344.6"
},
"time": "2025-05-30T18:11:02+00:00"
"time": "2025-06-12T18:03:59+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -1740,16 +1740,16 @@
},
{
"name": "laravel/framework",
"version": "v11.45.0",
"version": "v11.45.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "d0730deb427632004d24801be7ca1ed2c10fbc4e"
"reference": "b09ba32795b8e71df10856a2694706663984a239"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/d0730deb427632004d24801be7ca1ed2c10fbc4e",
"reference": "d0730deb427632004d24801be7ca1ed2c10fbc4e",
"url": "https://api.github.com/repos/laravel/framework/zipball/b09ba32795b8e71df10856a2694706663984a239",
"reference": "b09ba32795b8e71df10856a2694706663984a239",
"shasum": ""
},
"require": {
@@ -1951,7 +1951,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-05-20T15:15:58+00:00"
"time": "2025-06-03T14:01:40+00:00"
},
{
"name": "laravel/prompts",
@@ -3285,16 +3285,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.9.1",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "ced71f79398ece168e24f7f7710462f462310d4d"
"reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d",
"reference": "ced71f79398ece168e24f7f7710462f462310d4d",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
"reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
"shasum": ""
},
"require": {
@@ -3302,9 +3302,9 @@
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
"symfony/clock": "^6.3 || ^7.0",
"symfony/clock": "^6.3.12 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0"
"symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -3312,14 +3312,13 @@
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.57.2",
"friendsofphp/php-cs-fixer": "^3.75.0",
"kylekatarnls/multi-tester": "^2.5.3",
"ondrejmirtes/better-reflection": "^6.25.0.4",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.11.2",
"phpunit/phpunit": "^10.5.20",
"squizlabs/php_codesniffer": "^3.9.0"
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.17",
"phpunit/phpunit": "^10.5.46",
"squizlabs/php_codesniffer": "^3.13.0"
},
"bin": [
"bin/carbon"
@@ -3387,7 +3386,7 @@
"type": "tidelift"
}
],
"time": "2025-05-01T19:51:51+00:00"
"time": "2025-06-12T10:24:28+00:00"
},
{
"name": "nette/schema",
@@ -3453,16 +3452,16 @@
},
{
"name": "nette/utils",
"version": "v4.0.6",
"version": "v4.0.7",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "ce708655043c7050eb050df361c5e313cf708309"
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309",
"reference": "ce708655043c7050eb050df361c5e313cf708309",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"shasum": ""
},
"require": {
@@ -3533,9 +3532,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.6"
"source": "https://github.com/nette/utils/tree/v4.0.7"
},
"time": "2025-03-30T21:06:30+00:00"
"time": "2025-06-03T04:55:08+00:00"
},
{
"name": "nikic/php-parser",
@@ -4775,20 +4774,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.7.6",
"version": "4.8.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088"
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"ext-json": "*",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
@@ -4797,26 +4796,23 @@
"rhumsaa/uuid": "self.version"
},
"require-dev": {
"captainhook/captainhook": "^5.10",
"captainhook/captainhook": "^5.25",
"captainhook/plugin-composer": "^5.3",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.8",
"ergebnis/composer-normalize": "^2.15",
"mockery/mockery": "^1.3",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"ergebnis/composer-normalize": "^2.47",
"mockery/mockery": "^1.6",
"paragonie/random-lib": "^2",
"php-mock/php-mock": "^2.2",
"php-mock/php-mock-mockery": "^1.3",
"php-parallel-lint/php-parallel-lint": "^1.1",
"phpbench/phpbench": "^1.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9",
"ramsey/composer-repl": "^1.4",
"slevomat/coding-standard": "^8.4",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.9"
"php-mock/php-mock": "^2.6",
"php-mock/php-mock-mockery": "^1.5",
"php-parallel-lint/php-parallel-lint": "^1.4.0",
"phpbench/phpbench": "^1.2.14",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-mockery": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"slevomat/coding-standard": "^8.18",
"squizlabs/php_codesniffer": "^3.13"
},
"suggest": {
"ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
@@ -4851,19 +4847,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.7.6"
"source": "https://github.com/ramsey/uuid/tree/4.8.1"
},
"funding": [
{
"url": "https://github.com/ramsey",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
"type": "tidelift"
}
],
"time": "2024-04-27T21:32:50+00:00"
"time": "2025-06-01T06:28:46+00:00"
},
{
"name": "robrichards/xmlseclibs",
@@ -7906,16 +7892,16 @@
},
{
"name": "filp/whoops",
"version": "2.18.0",
"version": "2.18.2",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e"
"reference": "89dabca1490bc77dbcab41c2b20968c7e44bf7c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
"reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
"url": "https://api.github.com/repos/filp/whoops/zipball/89dabca1490bc77dbcab41c2b20968c7e44bf7c3",
"reference": "89dabca1490bc77dbcab41c2b20968c7e44bf7c3",
"shasum": ""
},
"require": {
@@ -7965,7 +7951,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.18.0"
"source": "https://github.com/filp/whoops/tree/2.18.2"
},
"funding": [
{
@@ -7973,7 +7959,7 @@
"type": "github"
}
],
"time": "2025-03-15T12:00:00+00:00"
"time": "2025-06-11T20:42:19+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -8145,16 +8131,16 @@
},
{
"name": "larastan/larastan",
"version": "v3.4.0",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222"
"reference": "36706736a0c51d3337478fab9c919d78d2e03404"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/1042fa0c2ee490bb6da7381f3323f7292ad68222",
"reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222",
"url": "https://api.github.com/repos/larastan/larastan/zipball/36706736a0c51d3337478fab9c919d78d2e03404",
"reference": "36706736a0c51d3337478fab9c919d78d2e03404",
"shasum": ""
},
"require": {
@@ -8222,7 +8208,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.4.0"
"source": "https://github.com/larastan/larastan/tree/v3.4.2"
},
"funding": [
{
@@ -8230,7 +8216,7 @@
"type": "github"
}
],
"time": "2025-04-22T09:44:59+00:00"
"time": "2025-06-10T09:34:58+00:00"
},
{
"name": "mockery/mockery",
@@ -8377,23 +8363,23 @@
},
{
"name": "nunomaduro/collision",
"version": "v8.8.0",
"version": "v8.8.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
"reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8"
"reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8",
"reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/44ccb82e3e21efb5446748d2a3c81a030ac22bd5",
"reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5",
"shasum": ""
},
"require": {
"filp/whoops": "^2.18.0",
"nunomaduro/termwind": "^2.3.0",
"filp/whoops": "^2.18.1",
"nunomaduro/termwind": "^2.3.1",
"php": "^8.2.0",
"symfony/console": "^7.2.5"
"symfony/console": "^7.3.0"
},
"conflict": {
"laravel/framework": "<11.44.2 || >=13.0.0",
@@ -8401,15 +8387,15 @@
},
"require-dev": {
"brianium/paratest": "^7.8.3",
"larastan/larastan": "^3.2",
"laravel/framework": "^11.44.2 || ^12.6",
"laravel/pint": "^1.21.2",
"laravel/sail": "^1.41.0",
"laravel/sanctum": "^4.0.8",
"larastan/larastan": "^3.4.2",
"laravel/framework": "^11.44.2 || ^12.18",
"laravel/pint": "^1.22.1",
"laravel/sail": "^1.43.1",
"laravel/sanctum": "^4.1.1",
"laravel/tinker": "^2.10.1",
"orchestra/testbench-core": "^9.12.0 || ^10.1",
"pestphp/pest": "^3.8.0",
"sebastian/environment": "^7.2.0 || ^8.0"
"orchestra/testbench-core": "^9.12.0 || ^10.4",
"pestphp/pest": "^3.8.2",
"sebastian/environment": "^7.2.1 || ^8.0"
},
"type": "library",
"extra": {
@@ -8472,7 +8458,7 @@
"type": "patreon"
}
],
"time": "2025-04-03T14:33:09+00:00"
"time": "2025-06-11T01:04:21+00:00"
},
{
"name": "phar-io/manifest",
@@ -8975,16 +8961,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.21",
"version": "11.5.23",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d565e2cdc21a7db9dc6c399c1fc2083b8010f289"
"reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d565e2cdc21a7db9dc6c399c1fc2083b8010f289",
"reference": "d565e2cdc21a7db9dc6c399c1fc2083b8010f289",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86ebcd8a3dbcd1857d88505109b2a2b376501cde",
"reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde",
"shasum": ""
},
"require": {
@@ -9056,7 +9042,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.21"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.23"
},
"funding": [
{
@@ -9080,7 +9066,7 @@
"type": "tidelift"
}
],
"time": "2025-05-21T12:35:00+00:00"
"time": "2025-06-13T05:47:49+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -10022,16 +10008,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.13.0",
"version": "3.13.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "65ff2489553b83b4597e89c3b8b721487011d186"
"reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186",
"reference": "65ff2489553b83b4597e89c3b8b721487011d186",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1b71b4dd7e7ef651ac749cea67e513c0c832f4bd",
"reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd",
"shasum": ""
},
"require": {
@@ -10102,7 +10088,7 @@
"type": "thanks_dev"
}
],
"time": "2025-05-11T03:36:00+00:00"
"time": "2025-06-12T15:04:34+00:00"
},
{
"name": "ssddanbrown/asserthtml",

View File

@@ -486,7 +486,7 @@ Link: https://github.com/ramsey/collection.git
ramsey/uuid
License: MIT
License File: vendor/ramsey/uuid/LICENSE
Copyright: Copyright (c) 2012-2023 Ben Ramsey <***@*********.***>
Copyright: Copyright (c) 2012-2025 Ben Ramsey <***@*********.***>
Source: https://github.com/ramsey/uuid.git
Link: https://github.com/ramsey/uuid.git
-----------

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Vytvořit',
'update' => 'Aktualizovat',
'edit' => 'Upravit',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archivovat',
'unarchive' => 'Od-Archivovat',
'sort' => 'Seřadit',
'move' => 'Přesunout',
'copy' => 'Kopírovat',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stabilní obsah)',
'pages_edit_switch_to_wysiwyg' => 'Přepnout na WYSIWYG Editor',
'pages_edit_switch_to_new_wysiwyg' => 'Přepnout na nový WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(V beta testování)',
'pages_edit_set_changelog' => 'Nastavit protokol změn',
'pages_edit_enter_changelog_desc' => 'Zadejte stručný popis změn, které jste provedli',
'pages_edit_enter_changelog' => 'Zadejte protokol změn',
@@ -392,11 +392,11 @@ return [
'comment' => 'Komentář',
'comments' => 'Komentáře',
'comment_add' => 'Přidat komentář',
'comment_none' => 'No comments to display',
'comment_none' => 'Žádné komentáře k zobrazení',
'comment_placeholder' => 'Zde zadejte komentář',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count vlákno komentáře|:count vláken komentářů',
'comment_archived_count' => ':count archivováno',
'comment_archived_threads' => 'Archivovaná vlákna',
'comment_save' => 'Uložit komentář',
'comment_new' => 'Nový komentář',
'comment_created' => 'komentováno :createDiff',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Komentář odstraněn',
'comment_created_success' => 'Komentář přidán',
'comment_updated_success' => 'Komentář aktualizován',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Komentář archivován',
'comment_unarchive_success' => 'Komentář od-archivován',
'comment_view' => 'Zobrazit komentář',
'comment_jump_to_thread' => 'Přejít na vlákno',
'comment_delete_confirm' => 'Opravdu chcete odstranit tento komentář?',
'comment_in_reply_to' => 'Odpověď na :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Odkaz',
'comment_reference_outdated' => '(Zastaralý)',
'comment_editor_explain' => 'Zde jsou komentáře, které zůstaly na této stránce. Komentáře lze přidat a spravovat při prohlížení uložené stránky.',
// Revision

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Erstellen',
'update' => 'Aktualisieren',
'edit' => 'Bearbeiten',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archivieren',
'unarchive' => 'Nicht mehr archivieren',
'sort' => 'Sortieren',
'move' => 'Verschieben',
'copy' => 'Kopieren',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)',
'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln',
'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(Im Beta-Test)',
'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
@@ -392,11 +392,11 @@ return [
'comment' => 'Kommentar',
'comments' => 'Kommentare',
'comment_add' => 'Kommentieren',
'comment_none' => 'No comments to display',
'comment_none' => 'Keine Kommentare vorhanden',
'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count Thema|:count Themen',
'comment_archived_count' => ':count archiviert',
'comment_archived_threads' => 'Archivierte Themen',
'comment_save' => 'Kommentar speichern',
'comment_new' => 'Neuer Kommentar',
'comment_created' => ':createDiff kommentiert',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Kommentar gelöscht',
'comment_created_success' => 'Kommentar hinzugefügt',
'comment_updated_success' => 'Kommentar aktualisiert',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Kommentar archiviert',
'comment_unarchive_success' => 'Kommentar nicht mehr archiviert',
'comment_view' => 'Kommentar ansehen',
'comment_jump_to_thread' => 'Zum Thema springen',
'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',
'comment_in_reply_to' => 'Antwort auf :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Referenz',
'comment_reference_outdated' => '(Veraltet)',
'comment_editor_explain' => 'Hier sind die Kommentare, die auf dieser Seite hinterlassen wurden. Kommentare können hinzugefügt und verwaltet werden, wenn die gespeicherte Seite angezeigt wird.',
// Revision

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Anlegen',
'update' => 'Aktualisieren',
'edit' => 'Bearbeiten',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archivieren',
'unarchive' => 'Nicht mehr archivieren',
'sort' => 'Sortieren',
'move' => 'Verschieben',
'copy' => 'Kopieren',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)',
'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln',
'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(Im Beta-Test)',
'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
'pages_edit_enter_changelog_desc' => 'Bitte gib eine kurze Zusammenfassung deiner Änderungen ein',
'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
@@ -392,11 +392,11 @@ return [
'comment' => 'Kommentar',
'comments' => 'Kommentare',
'comment_add' => 'Kommentieren',
'comment_none' => 'No comments to display',
'comment_none' => 'Keine Kommentare vorhanden',
'comment_placeholder' => 'Gib hier deine Kommentare ein',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count Thema|:count Themen',
'comment_archived_count' => ':count archiviert',
'comment_archived_threads' => 'Archivierte Themen',
'comment_save' => 'Kommentar speichern',
'comment_new' => 'Neuer Kommentar',
'comment_created' => ':createDiff kommentiert',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Kommentar gelöscht',
'comment_created_success' => 'Kommentar hinzugefügt',
'comment_updated_success' => 'Kommentar aktualisiert',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Kommentar archiviert',
'comment_unarchive_success' => 'Kommentar nicht mehr archiviert',
'comment_view' => 'Kommentar ansehen',
'comment_jump_to_thread' => 'Zum Thema springen',
'comment_delete_confirm' => 'Möchtst du diesen Kommentar wirklich löschen?',
'comment_in_reply_to' => 'Antwort auf :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Referenz',
'comment_reference_outdated' => '(Veraltet)',
'comment_editor_explain' => 'Hier sind die Kommentare, die auf dieser Seite hinterlassen wurden. Kommentare können hinzugefügt und verwaltet werden, wenn die gespeicherte Seite angezeigt wird.',
// Revision

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Crear',
'update' => 'Actualizar',
'edit' => 'Editar',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archivar',
'unarchive' => 'Desarchivar',
'sort' => 'Ordenar',
'move' => 'Mover',
'copy' => 'Copiar',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Contenido Estable)',
'pages_edit_switch_to_wysiwyg' => 'Cambiar a Editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg' => 'Cambiar a nuevo editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(En prueba beta)',
'pages_edit_set_changelog' => 'Ajustar Log de cambios',
'pages_edit_enter_changelog_desc' => 'Introduzca una breve descripción de los cambios que ha realizado',
'pages_edit_enter_changelog' => 'Entrar al Log de cambios',
@@ -392,11 +392,11 @@ return [
'comment' => 'Comentario',
'comments' => 'Comentarios',
'comment_add' => 'Añadir Comentario',
'comment_none' => 'No comments to display',
'comment_none' => 'No hay comentarios para mostrar',
'comment_placeholder' => 'Introduzca su comentario aquí',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count hilo de comentarios|:count hilos de comentarios',
'comment_archived_count' => ':count Archivados',
'comment_archived_threads' => 'Hilos archivados',
'comment_save' => 'Guardar comentario',
'comment_new' => 'Nuevo Comentario',
'comment_created' => 'comentado :createDiff',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Comentario borrado',
'comment_created_success' => 'Comentario añadido',
'comment_updated_success' => 'Comentario actualizado',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Comentario archivado',
'comment_unarchive_success' => 'Comentario desarchivado',
'comment_view' => 'Ver comentario',
'comment_jump_to_thread' => 'Ir al hilo',
'comment_delete_confirm' => '¿Está seguro de que quiere borrar este comentario?',
'comment_in_reply_to' => 'En respuesta a :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Referencia',
'comment_reference_outdated' => '(obsoleto)',
'comment_editor_explain' => 'Estos son los comentarios que se han escrito en esta página. Los comentarios se pueden añadir y administrar cuando se ve la página guardada.',
// Revision

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Contenido Estable)',
'pages_edit_switch_to_wysiwyg' => 'Cambiar a Editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg' => 'Cambiar a nuevo editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(En prueba beta)',
'pages_edit_set_changelog' => 'Establecer cambios de registro',
'pages_edit_enter_changelog_desc' => 'Introduzca una breve descripción de los cambios que ha realizado',
'pages_edit_enter_changelog' => 'Entrar en cambio de registro',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stabiilne sisu)',
'pages_edit_switch_to_wysiwyg' => 'Kasuta WYSIWYG redaktorit',
'pages_edit_switch_to_new_wysiwyg' => 'Kasuta uut tekstiredaktorit',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(beetatestimisel)',
'pages_edit_set_changelog' => 'Muudatuste logi',
'pages_edit_enter_changelog_desc' => 'Sisesta tehtud muudatuste lühikirjeldus',
'pages_edit_enter_changelog' => 'Salvesta muudatuste logi',

View File

@@ -10,7 +10,7 @@ return [
// Auth
'error_user_exists_different_creds' => 'Sähköpostiosoite :email on jo käytössä toisessa käyttäjätunnuksessa.',
'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',
'auth_pre_register_theme_prevention' => 'Käyttäjätiliä ei voitu rekisteröidä annetuille tiedoille',
'email_already_confirmed' => 'Sähköposti on jo vahvistettu, yritä kirjautua sisään.',
'email_confirmation_invalid' => 'Tämä vahvistuslinkki ei ole voimassa tai sitä on jo käytetty, yritä rekisteröityä uudelleen.',
'email_confirmation_expired' => 'Vahvistuslinkki on vanhentunut, uusi vahvistussähköposti on lähetetty.',
@@ -38,7 +38,7 @@ Sovellus ei tunnista ulkoisen todennuspalvelun pyyntöä. Ongelman voi aiheuttaa
'social_driver_not_found' => 'Sosiaalisen median tilin ajuria ei löytynyt',
'social_driver_not_configured' => ':socialAccount-tilin asetuksia ei ole määritetty oikein.',
'invite_token_expired' => 'Tämä kutsulinkki on vanhentunut. Voit sen sijaan yrittää palauttaa tilisi salasanan.',
'login_user_not_found' => 'A user for this action could not be found.',
'login_user_not_found' => 'Käyttäjää tälle toiminnolle ei löytynyt.',
// System
'path_not_writable' => 'Tiedostopolkuun :filePath ei voitu ladata tiedostoa. Tarkista polun kirjoitusoikeudet.',
@@ -79,7 +79,7 @@ Sovellus ei tunnista ulkoisen todennuspalvelun pyyntöä. Ongelman voi aiheuttaa
// Users
'users_cannot_delete_only_admin' => 'Ainoaa ylläpitäjää ei voi poistaa',
'users_cannot_delete_guest' => 'Vieraskäyttäjää ei voi poistaa',
'users_could_not_send_invite' => 'Could not create user since invite email failed to send',
'users_could_not_send_invite' => 'Käyttäjää ei voitu luoda kutsun lähettämisen jälkeen',
// Roles
'role_cannot_be_edited' => 'Tätä roolia ei voi muokata',
@@ -107,16 +107,16 @@ Sovellus ei tunnista ulkoisen todennuspalvelun pyyntöä. Ongelman voi aiheuttaa
'back_soon' => 'Se palautetaan pian.',
// Import
'import_zip_cant_read' => 'Could not read ZIP file.',
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
'import_validation_failed' => 'Import ZIP failed to validate with errors:',
'import_zip_failed_notification' => 'Failed to import ZIP file.',
'import_perms_books' => 'You are lacking the required permissions to create books.',
'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',
'import_perms_pages' => 'You are lacking the required permissions to create pages.',
'import_perms_images' => 'You are lacking the required permissions to create images.',
'import_perms_attachments' => 'You are lacking the required permission to create attachments.',
'import_zip_cant_read' => 'ZIP-tiedostoa ei voitu lukea.',
'import_zip_cant_decode_data' => 'ZIP-tiedoston data.json sisältöä ei löydy eikä sitä voitu purkaa.',
'import_zip_no_data' => 'ZIP-tiedostoilla ei ole odotettua kirjaa, lukua tai sivun sisältöä.',
'import_validation_failed' => 'Tuonti ZIP epäonnistui virheiden kanssa:',
'import_zip_failed_notification' => 'ZIP-tiedoston tuominen epäonnistui.',
'import_perms_books' => 'Sinulla ei ole tarvittavia oikeuksia luoda kirjoja.',
'import_perms_chapters' => 'Sinulla ei ole tarvittavia oikeuksia luoda kappaleita.',
'import_perms_pages' => 'Sinulla ei ole tarvittavia oikeuksia luoda sivuja.',
'import_perms_images' => 'Sinulla ei ole tarvittavia oikeuksia luoda kuvia.',
'import_perms_attachments' => 'Sinulla ei ole tarvittavaa lupaa luoda liitteitä.',
// API errors
'api_no_authorization_found' => 'Pyynnöstä ei löytynyt valtuutuskoodia',

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Crea',
'update' => 'Aggiorna',
'edit' => 'Modifica',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archivia',
'unarchive' => 'Ripristina',
'sort' => 'Ordina',
'move' => 'Sposta',
'copy' => 'Copia',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Contenuto stabile)',
'pages_edit_switch_to_wysiwyg' => 'Passa all\'editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg' => 'Passa al nuovo WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Test)',
'pages_edit_set_changelog' => 'Imposta changelog',
'pages_edit_enter_changelog_desc' => 'Inserisci una breve descrizione dei cambiamenti che hai apportato',
'pages_edit_enter_changelog' => 'Inserisci changelog',
@@ -392,11 +392,11 @@ return [
'comment' => 'Commento',
'comments' => 'Commenti',
'comment_add' => 'Aggiungi commento',
'comment_none' => 'No comments to display',
'comment_none' => 'Nessun commento da visualizzare',
'comment_placeholder' => 'Scrivi un commento',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_archived_count' => ':count Archiviato',
'comment_archived_threads' => 'Discussioni Archiviate',
'comment_save' => 'Salva commento',
'comment_new' => 'Nuovo commento',
'comment_created' => 'ha commentato :createDiff',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Commento eliminato',
'comment_created_success' => 'Commento aggiunto',
'comment_updated_success' => 'Commento aggiornato',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Commento archiviato',
'comment_unarchive_success' => 'Commento ripristinato',
'comment_view' => 'Visualizza commento',
'comment_jump_to_thread' => 'Vai al thread',
'comment_delete_confirm' => 'Sei sicuro di voler eliminare questo commento?',
'comment_in_reply_to' => 'In risposta a :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Riferimento',
'comment_reference_outdated' => '(Obsoleto)',
'comment_editor_explain' => 'Ecco i commenti che sono stati lasciati in questa pagina. I commenti possono essere aggiunti e gestiti quando si visualizza la pagina salvata.',
// Revision

View File

@@ -30,8 +30,8 @@ return [
'create' => '作成',
'update' => '更新',
'edit' => '編集',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'アーカイブ',
'unarchive' => 'アーカイブ解除',
'sort' => '並び順',
'move' => '移動',
'copy' => 'コピー',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(安定したコンテンツ)',
'pages_edit_switch_to_wysiwyg' => 'WYSIWYGエディタに切り替え',
'pages_edit_switch_to_new_wysiwyg' => '新しいWYSIWYGエディタに切り替える',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(ベータテスト版)',
'pages_edit_set_changelog' => '編集内容についての説明',
'pages_edit_enter_changelog_desc' => 'どのような変更を行ったのかを記録してください',
'pages_edit_enter_changelog' => '編集内容を入力',
@@ -393,11 +393,11 @@ return [
'comment' => 'コメント',
'comments' => 'コメント',
'comment_add' => 'コメント追加',
'comment_none' => 'No comments to display',
'comment_none' => '表示するコメントがありません',
'comment_placeholder' => 'コメントを記入してください',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count 個のコメントスレッド|:count 個のコメントスレッド',
'comment_archived_count' => ':count 個のアーカイブ',
'comment_archived_threads' => 'アーカイブされたスレッド',
'comment_save' => 'コメントを保存',
'comment_new' => '新規コメント作成',
'comment_created' => 'コメントを作成しました :createDiff',
@@ -406,14 +406,14 @@ return [
'comment_deleted_success' => 'コメントを削除しました',
'comment_created_success' => 'コメントを追加しました',
'comment_updated_success' => 'コメントを更新しました',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'コメントをアーカイブしました',
'comment_unarchive_success' => 'コメントのアーカイブを解除しました',
'comment_view' => 'コメントを表示',
'comment_jump_to_thread' => 'スレッドにジャンプ',
'comment_delete_confirm' => '本当にこのコメントを削除しますか?',
'comment_in_reply_to' => ':commentIdへ返信',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => '参照箇所',
'comment_reference_outdated' => '(以前の記述)',
'comment_editor_explain' => 'ここにはページに付けられたコメントを表示します。 コメントの追加と管理は保存されたページの表示時に行うことができます。',
// Revision

View File

@@ -30,8 +30,8 @@ return [
'create' => 'Aanmaken',
'update' => 'Bijwerken',
'edit' => 'Bewerk',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Archiveer',
'unarchive' => 'Terughalen',
'sort' => 'Sorteer',
'move' => 'Verplaats',
'copy' => 'Kopieer',

View File

@@ -23,7 +23,7 @@ return [
'meta_updated' => 'Bijgewerkt: :timeLength',
'meta_updated_name' => 'Bijgewerkt: :timeLength door :user',
'meta_owned_name' => 'Eigendom van :user',
'meta_reference_count' => 'Gerefereerd door :count item|Gerefereerd door :count items',
'meta_reference_count' => 'Verwijzing in :count item|Verwijzing in :count items',
'entity_select' => 'Entiteit selecteren',
'entity_select_lack_permission' => 'Je hebt niet de vereiste machtiging om dit item te selecteren',
'images' => 'Afbeeldingen',
@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stabiele Inhoud)',
'pages_edit_switch_to_wysiwyg' => 'Schakel naar de WYSIWYG Bewerker',
'pages_edit_switch_to_new_wysiwyg' => 'Schakel naar de nieuwe WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta-testfase)',
'pages_edit_set_changelog' => 'Logboek instellen',
'pages_edit_enter_changelog_desc' => 'Geef een korte omschrijving van de wijzigingen die je gemaakt hebt',
'pages_edit_enter_changelog' => 'Voeg toe aan logboek',
@@ -392,11 +392,11 @@ return [
'comment' => 'Reactie',
'comments' => 'Reacties',
'comment_add' => 'Reactie toevoegen',
'comment_none' => 'No comments to display',
'comment_none' => 'Geen opmerkingen om weer te geven',
'comment_placeholder' => 'Laat hier een reactie achter',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count Reactie Thread|:count Reactie Threads',
'comment_archived_count' => ':count Gearchiveerd',
'comment_archived_threads' => 'Gearchiveerde Threads',
'comment_save' => 'Sla reactie op',
'comment_new' => 'Nieuwe reactie',
'comment_created' => 'reactie gegeven :createDiff',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Reactie verwijderd',
'comment_created_success' => 'Reactie toegevoegd',
'comment_updated_success' => 'Reactie bijgewerkt',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Opmerking gearchiveerd',
'comment_unarchive_success' => 'Opmerking teruggehaald',
'comment_view' => 'Opmerking weergeven',
'comment_jump_to_thread' => 'Ga naar thread',
'comment_delete_confirm' => 'Weet je zeker dat je deze reactie wilt verwijderen?',
'comment_in_reply_to' => 'Als antwoord op :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Verwijzing',
'comment_reference_outdated' => '(Verouderd)',
'comment_editor_explain' => 'Hier zijn de opmerkingen die zijn achtergelaten op deze pagina. Opmerkingen kunnen worden toegevoegd en beheerd wanneer u de opgeslagen pagina bekijkt.',
// Revision

View File

@@ -35,8 +35,8 @@ return [
'create' => 'Criar',
'update' => 'Atualizar',
'edit' => 'Editar',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'archive' => 'Arquivar',
'unarchive' => 'Desarquivar',
'sort' => 'Ordenar',
'move' => 'Mover',
'copy' => 'Copiar',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Conteúdo Estável)',
'pages_edit_switch_to_wysiwyg' => 'Alternar para o Editor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg' => 'Mudar para o novo WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(Em teste beta)',
'pages_edit_set_changelog' => 'Relatar Alterações',
'pages_edit_enter_changelog_desc' => 'Digite uma breve descrição das alterações efetuadas por você',
'pages_edit_enter_changelog' => 'Insira Alterações',
@@ -392,11 +392,11 @@ return [
'comment' => 'Comentário',
'comments' => 'Comentários',
'comment_add' => 'Adicionar Comentário',
'comment_none' => 'No comments to display',
'comment_none' => 'Nenhum comentário para exibir',
'comment_placeholder' => 'Digite seus comentários aqui',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_thread_count' => ':count Tópico de Comentário|:count Tópicos de Comentários',
'comment_archived_count' => ':count Arquivado',
'comment_archived_threads' => 'Tópicos Arquivados',
'comment_save' => 'Salvar comentário',
'comment_new' => 'Novo Comentário',
'comment_created' => 'comentado :createDiff',
@@ -405,14 +405,14 @@ return [
'comment_deleted_success' => 'Comentário removido',
'comment_created_success' => 'Comentário adicionado',
'comment_updated_success' => 'Comentário editado',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_archive_success' => 'Comentário arquivado',
'comment_unarchive_success' => 'Comentário desarquivado',
'comment_view' => 'Ver comentário',
'comment_jump_to_thread' => 'Ir para o tópico',
'comment_delete_confirm' => 'Você tem certeza de que deseja excluir este comentário?',
'comment_in_reply_to' => 'Em resposta à :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_reference' => 'Referência',
'comment_reference_outdated' => '(Desatualizado)',
'comment_editor_explain' => 'Aqui estão os comentários que foram deixados nesta página. Comentários podem ser adicionados e gerenciados ao visualizar a página salva.',
// Revision

View File

@@ -47,7 +47,7 @@ return [
'bookshelf_update' => '更新書棧',
'bookshelf_update_notification' => '書棧已更新',
'bookshelf_delete' => '刪除書棧',
'bookshelf_delete_notification' => '書已刪除',
'bookshelf_delete_notification' => '書已刪除',
// Revisions
'revision_restore' => '還原的版本',

View File

@@ -67,7 +67,7 @@ return [
// Entities
'entity_not_found' => '找不到實體',
'bookshelf_not_found' => '未找到書',
'bookshelf_not_found' => '未找到書',
'book_not_found' => '找不到書本',
'page_not_found' => '找不到頁面',
'chapter_not_found' => '找不到章節',

View File

@@ -40,7 +40,7 @@ export class PageComment extends Component {
this.commentId = this.$opts.commentId;
this.commentLocalId = this.$opts.commentLocalId;
this.deletedText = this.$opts.deletedText;
this.deletedText = this.$opts.deletedText;
this.updatedText = this.$opts.updatedText;
this.archiveText = this.$opts.archiveText;
// Editor reference and text options

View File

@@ -84,7 +84,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
// @ts-ignore
window.debugEditorState = () => {
console.log(editor.getEditorState().toJSON());
return editor.getEditorState().toJSON();
};
registerCommonNodeMutationListeners(context);

View File

@@ -355,6 +355,7 @@ function onSelectionChange(
lastNode instanceof ParagraphNode &&
lastNode.getChildrenSize() === 0
) {
selection.format = lastNode.getTextFormat();
selection.style = lastNode.getTextStyle();
} else {
selection.format = 0;

View File

@@ -1069,6 +1069,7 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
@@ -1149,6 +1150,7 @@ describe('LexicalEditor tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});

View File

@@ -76,6 +76,7 @@ describe('LexicalEditorState tests', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});
@@ -111,7 +112,7 @@ describe('LexicalEditorState tests', () => {
});
expect(JSON.stringify(editor.getEditorState().toJSON())).toEqual(
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"id":"","alignment":"","inset":0,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"type":"paragraph","version":1,"id":"","alignment":"","inset":0,"textFormat":0,"textStyle":""}],"direction":null,"type":"root","version":1}}`,
);
});

File diff suppressed because one or more lines are too long

View File

@@ -38,6 +38,7 @@ import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
type TestEnv = {
readonly container: HTMLDivElement;
@@ -487,6 +488,7 @@ export function createTestContext(): EditorUiContext {
theme: {},
nodes: [
ImageNode,
MediaNode,
]
});

View File

@@ -19,7 +19,7 @@ import type {
LexicalNode,
NodeKey,
} from '../LexicalNode';
import type {RangeSelection} from 'lexical';
import {RangeSelection, TEXT_TYPE_TO_FORMAT, TextFormatType} from 'lexical';
import {
$applyNodeReplacement,
@@ -36,6 +36,7 @@ import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} f
export type SerializedParagraphNode = Spread<
{
textFormat: number;
textStyle: string;
},
SerializedCommonBlockNode
@@ -45,10 +46,12 @@ export type SerializedParagraphNode = Spread<
export class ParagraphNode extends CommonBlockNode {
['constructor']!: KlassConstructor<typeof ParagraphNode>;
/** @internal */
__textFormat: number;
__textStyle: string;
constructor(key?: NodeKey) {
super(key);
this.__textFormat = 0;
this.__textStyle = '';
}
@@ -56,6 +59,22 @@ export class ParagraphNode extends CommonBlockNode {
return 'paragraph';
}
getTextFormat(): number {
const self = this.getLatest();
return self.__textFormat;
}
setTextFormat(type: number): this {
const self = this.getWritable();
self.__textFormat = type;
return self;
}
hasTextFormat(type: TextFormatType): boolean {
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
return (this.getTextFormat() & formatFlag) !== 0;
}
getTextStyle(): string {
const self = this.getLatest();
return self.__textStyle;
@@ -73,6 +92,7 @@ export class ParagraphNode extends CommonBlockNode {
afterCloneFrom(prevNode: this) {
super.afterCloneFrom(prevNode);
this.__textFormat = prevNode.__textFormat;
this.__textStyle = prevNode.__textStyle;
copyCommonBlockProperties(prevNode, this);
}
@@ -125,12 +145,14 @@ export class ParagraphNode extends CommonBlockNode {
static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
const node = $createParagraphNode();
deserializeCommonBlockNode(serializedNode, node);
node.setTextFormat(serializedNode.textFormat);
return node;
}
exportJSON(): SerializedParagraphNode {
return {
...super.exportJSON(),
textFormat: this.getTextFormat(),
textStyle: this.getTextStyle(),
type: 'paragraph',
version: 1,
@@ -144,6 +166,7 @@ export class ParagraphNode extends CommonBlockNode {
restoreSelection: boolean,
): ParagraphNode {
const newElement = $createParagraphNode();
newElement.setTextFormat(rangeSelection.format);
newElement.setTextStyle(rangeSelection.style);
const direction = this.getDirection();
newElement.setDirection(direction);

View File

@@ -620,6 +620,7 @@ export class TextNode extends LexicalNode {
// HTML content and not have the ability to use CSS classes.
exportDOM(editor: LexicalEditor): DOMExportOutput {
let {element} = super.exportDOM(editor);
const originalElementName = (element?.nodeName || '').toLowerCase()
invariant(
element !== null && isHTMLElement(element),
'Expected TextNode createDOM to always return a HTMLElement',
@@ -649,8 +650,8 @@ export class TextNode extends LexicalNode {
// This is the only way to properly add support for most clients,
// even if it's semantically incorrect to have to resort to using
// <b>, <u>, <s>, <i> elements.
if (this.hasFormat('bold')) {
element = wrapElementWith(element, 'b');
if (this.hasFormat('bold') && originalElementName !== 'strong') {
element = wrapElementWith(element, 'strong');
}
if (this.hasFormat('italic')) {
element = wrapElementWith(element, 'em');

View File

@@ -53,6 +53,7 @@ describe('LexicalParagraphNode tests', () => {
direction: null,
id: '',
inset: 0,
textFormat: 0,
textStyle: '',
type: 'paragraph',
version: 1,

View File

@@ -839,7 +839,7 @@ describe('LexicalTextNode tests', () => {
paragraph.append(textNode);
const html = $generateHtmlFromNodes($getEditor(), null);
expect(html).toBe('<p><u><em><b><code spellcheck="false"><strong>hello</strong></code></b></em></u></p>');
expect(html).toBe('<p><u><em><strong><code spellcheck="false"><strong>hello</strong></code></strong></em></u></p>');
});
});

View File

@@ -8,13 +8,12 @@ import {
} from 'lexical';
import type {EditorConfig} from "lexical/LexicalEditor";
import {el, setOrRemoveAttribute, sizeToPixels} from "../../utils/dom";
import {el, setOrRemoveAttribute, sizeToPixels, styleMapToStyleString, styleStringToStyleMap} from "../../utils/dom";
import {
CommonBlockAlignment, deserializeCommonBlockNode,
setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps
} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
@@ -46,6 +45,19 @@ function filterAttributes(attributes: Record<string, string>): Record<string, st
return filtered;
}
function removeStyleFromAttributes(attributes: Record<string, string>, styleName: string): Record<string, string> {
const attrCopy = Object.assign({}, attributes);
if (!attributes.style) {
return attrCopy;
}
const map = styleStringToStyleMap(attributes.style);
map.delete(styleName);
attrCopy.style = styleMapToStyleString(map);
return attrCopy;
}
function domElementToNode(tag: MediaNodeTag, element: HTMLElement): MediaNode {
const node = $createMediaNode(tag);
@@ -118,7 +130,7 @@ export class MediaNode extends ElementNode {
getAttributes(): Record<string, string> {
const self = this.getLatest();
return self.__attributes;
return Object.assign({}, self.__attributes);
}
setSources(sources: MediaNodeSource[]) {
@@ -128,25 +140,37 @@ export class MediaNode extends ElementNode {
getSources(): MediaNodeSource[] {
const self = this.getLatest();
return self.__sources;
return self.__sources.map(s => Object.assign({}, s))
}
setSrc(src: string): void {
const attrs = Object.assign({}, this.getAttributes());
const attrs = this.getAttributes();
const sources = this.getSources();
if (this.__tag ==='object') {
attrs.data = src;
} if (this.__tag === 'video' && sources.length > 0) {
sources[0].src = src;
delete attrs.src;
if (sources.length > 1) {
sources.splice(1, sources.length - 1);
}
this.setSources(sources);
} else {
attrs.src = src;
}
this.setAttributes(attrs);
}
setWidthAndHeight(width: string, height: string): void {
const attrs = Object.assign(
{},
let attrs: Record<string, string> = Object.assign(
this.getAttributes(),
{width, height},
);
attrs = removeStyleFromAttributes(attrs, 'width');
attrs = removeStyleFromAttributes(attrs, 'height');
this.setAttributes(attrs);
}
@@ -185,8 +209,8 @@ export class MediaNode extends ElementNode {
return;
}
const attrs = Object.assign({}, this.getAttributes(), {height});
this.setAttributes(attrs);
const attrs = Object.assign(this.getAttributes(), {height});
this.setAttributes(removeStyleFromAttributes(attrs, 'height'));
}
getHeight(): number {
@@ -195,8 +219,9 @@ export class MediaNode extends ElementNode {
}
setWidth(width: number): void {
const attrs = Object.assign({}, this.getAttributes(), {width});
this.setAttributes(attrs);
const existingAttrs = this.getAttributes();
const attrs: Record<string, string> = Object.assign(existingAttrs, {width});
this.setAttributes(removeStyleFromAttributes(attrs, 'width'));
}
getWidth(): number {
@@ -222,15 +247,9 @@ export class MediaNode extends ElementNode {
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const media = this.createInnerDOM();
const wrap = el('span', {
return el('span', {
class: media.className + ' editor-media-wrap',
}, [media]);
wrap.addEventListener('click', e => {
_editor.update(() => $selectSingleNode(this));
});
return wrap;
}
updateDOM(prevNode: MediaNode, dom: HTMLElement): boolean {

View File

@@ -0,0 +1,46 @@
import {createTestContext} from "lexical/__tests__/utils";
import {$createMediaNode} from "@lexical/rich-text/LexicalMediaNode";
describe('LexicalMediaNode', () => {
test('setWidth/setHeight/setWidthAndHeight functions remove relevant styles', () => {
const {editor} = createTestContext();
editor.updateAndCommit(() => {
const mediaMode = $createMediaNode('video');
const defaultStyles = {style: 'width:20px;height:40px;color:red'};
mediaMode.setAttributes(defaultStyles);
mediaMode.setWidth(60);
expect(mediaMode.getWidth()).toBe(60);
expect(mediaMode.getAttributes().style).toBe('height:40px;color:red');
mediaMode.setAttributes(defaultStyles);
mediaMode.setHeight(77);
expect(mediaMode.getHeight()).toBe(77);
expect(mediaMode.getAttributes().style).toBe('width:20px;color:red');
mediaMode.setAttributes(defaultStyles);
mediaMode.setWidthAndHeight('6', '7');
expect(mediaMode.getWidth()).toBe(6);
expect(mediaMode.getHeight()).toBe(7);
expect(mediaMode.getAttributes().style).toBe('color:red');
});
});
test('setSrc on video uses sources if existing', () => {
const {editor} = createTestContext();
editor.updateAndCommit(() => {
const mediaMode = $createMediaNode('video');
mediaMode.setAttributes({src: 'z'});
mediaMode.setSources([{src: 'a', type: 'video'}, {src: 'b', type: 'video'}]);
mediaMode.setSrc('c');
expect(mediaMode.getAttributes().src).toBeUndefined();
expect(mediaMode.getSources()).toHaveLength(1);
expect(mediaMode.getSources()[0].src).toBe('c');
});
});
});

View File

@@ -123,6 +123,7 @@ describe('table selection', () => {
__prev: null,
__size: 1,
__style: '',
__textFormat: 0,
__textStyle: '',
__type: 'paragraph',
});

View File

@@ -92,6 +92,7 @@ export const image: EditorButtonDefinition = {
context.editor.update(() => {
const link = $createLinkedImageNodeFromImageData(image);
$insertNodes([link]);
link.select();
});
})
});

View File

@@ -192,11 +192,17 @@ export function $showMediaForm(media: MediaNode|null, context: EditorUiContext):
let formDefaults = {};
if (media) {
const nodeAttrs = media.getAttributes();
const nodeDOM = media.exportDOM(context.editor).element;
const nodeHtml = (nodeDOM instanceof HTMLElement) ? nodeDOM.outerHTML : '';
formDefaults = {
src: nodeAttrs.src || nodeAttrs.data || '',
src: nodeAttrs.src || nodeAttrs.data || media.getSources()[0]?.src || '',
width: nodeAttrs.width,
height: nodeAttrs.height,
embed: '',
embed: nodeHtml,
// This is used so we can check for edits against the embed field on submit
embed_check: nodeHtml,
}
}
@@ -214,7 +220,8 @@ export const media: EditorFormDefinition = {
}));
const embedCode = (formData.get('embed') || '').toString().trim();
if (embedCode) {
const embedCheck = (formData.get('embed_check') || '').toString().trim();
if (embedCode && embedCode !== embedCheck) {
context.editor.update(() => {
const node = $createMediaNodeFromHtml(embedCode);
if (selectedNode && node) {
@@ -236,6 +243,7 @@ export const media: EditorFormDefinition = {
if (selectedNode) {
selectedNode.setSrc(src);
selectedNode.setWidthAndHeight(width, height);
context.manager.triggerFutureStateRefresh();
return;
}
@@ -281,6 +289,11 @@ export const media: EditorFormDefinition = {
name: 'embed',
type: 'textarea',
},
{
label: '',
name: 'embed_check',
type: 'hidden',
},
],
}
])

View File

@@ -224,6 +224,10 @@ export function getImageToolbarContent(): EditorUiElement[] {
return [new EditorButton(image)];
}
export function getMediaToolbarContent(): EditorUiElement[] {
return [new EditorButton(media)];
}
export function getLinkToolbarContent(): EditorUiElement[] {
return [
new EditorButton(link),

View File

@@ -11,7 +11,7 @@ import {el} from "../../utils/dom";
export interface EditorFormFieldDefinition {
label: string;
name: string;
type: 'text' | 'select' | 'textarea' | 'checkbox';
type: 'text' | 'select' | 'textarea' | 'checkbox' | 'hidden';
}
export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
@@ -67,6 +67,9 @@ export class EditorFormField extends EditorUiElement {
input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
} else if (this.definition.type === 'checkbox') {
input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'});
} else if (this.definition.type === 'hidden') {
input = el('input', {id, name: this.definition.name, type: 'hidden'});
return el('div', {hidden: 'true'}, [input]);
} else {
input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
}

View File

@@ -12,17 +12,21 @@ function isNodeWithSize(node: LexicalNode): node is NodeHasSize&LexicalNode {
class NodeResizer {
protected context: EditorUiContext;
protected dom: HTMLElement|null = null;
protected resizerDOM: HTMLElement|null = null;
protected targetNode: LexicalNode|null = null;
protected scrollContainer: HTMLElement;
protected mouseTracker: MouseDragTracker|null = null;
protected activeSelection: string = '';
protected loadAbortController = new AbortController();
constructor(context: EditorUiContext) {
this.context = context;
this.scrollContainer = context.scrollDOM;
this.onSelectionChange = this.onSelectionChange.bind(this);
this.onTargetDOMLoad = this.onTargetDOMLoad.bind(this);
context.manager.onSelectionChange(this.onSelectionChange);
}
@@ -34,12 +38,7 @@ class NodeResizer {
if (nodes.length === 1 && isNodeWithSize(nodes[0])) {
const node = nodes[0];
const nodeKey = node.getKey();
let nodeDOM = this.context.editor.getElementByKey(nodeKey);
if (nodeDOM && nodeDOM.nodeName === 'SPAN') {
nodeDOM = nodeDOM.firstElementChild as HTMLElement;
}
let nodeDOM = this.getTargetDOM(node)
if (nodeDOM) {
this.showForNode(node, nodeDOM);
@@ -47,56 +46,81 @@ class NodeResizer {
}
}
protected getTargetDOM(targetNode: LexicalNode|null): HTMLElement|null {
if (targetNode == null) {
return null;
}
let nodeDOM = this.context.editor.getElementByKey(targetNode.__key)
if (nodeDOM && nodeDOM.nodeName === 'SPAN') {
nodeDOM = nodeDOM.firstElementChild as HTMLElement;
}
return nodeDOM;
}
protected onTargetDOMLoad(): void {
this.updateResizerPosition();
}
teardown() {
this.context.manager.offSelectionChange(this.onSelectionChange);
this.hide();
}
protected showForNode(node: NodeHasSize&LexicalNode, dom: HTMLElement) {
this.dom = this.buildDOM();
protected showForNode(node: NodeHasSize&LexicalNode, targetDOM: HTMLElement) {
this.resizerDOM = this.buildDOM();
this.targetNode = node;
let ghost = el('span', {class: 'editor-node-resizer-ghost'});
if ($isImageNode(node)) {
ghost = el('img', {src: dom.getAttribute('src'), class: 'editor-node-resizer-ghost'});
ghost = el('img', {src: targetDOM.getAttribute('src'), class: 'editor-node-resizer-ghost'});
}
this.dom.append(ghost);
this.resizerDOM.append(ghost);
this.context.scrollDOM.append(this.dom);
this.updateDOMPosition(dom);
this.context.scrollDOM.append(this.resizerDOM);
this.updateResizerPosition();
this.mouseTracker = this.setupTracker(this.dom, node, dom);
this.mouseTracker = this.setupTracker(this.resizerDOM, node, targetDOM);
this.activeSelection = node.getKey();
if (targetDOM.matches('img, embed, iframe, object')) {
this.loadAbortController = new AbortController();
targetDOM.addEventListener('load', this.onTargetDOMLoad, { signal: this.loadAbortController.signal });
}
}
protected updateDOMPosition(nodeDOM: HTMLElement) {
if (!this.dom) {
protected updateResizerPosition() {
const targetDOM = this.getTargetDOM(this.targetNode);
if (!this.resizerDOM || !targetDOM) {
return;
}
const scrollAreaRect = this.scrollContainer.getBoundingClientRect();
const nodeRect = nodeDOM.getBoundingClientRect();
const nodeRect = targetDOM.getBoundingClientRect();
const top = nodeRect.top - (scrollAreaRect.top - this.scrollContainer.scrollTop);
const left = nodeRect.left - scrollAreaRect.left;
this.dom.style.top = `${top}px`;
this.dom.style.left = `${left}px`;
this.dom.style.width = nodeRect.width + 'px';
this.dom.style.height = nodeRect.height + 'px';
this.resizerDOM.style.top = `${top}px`;
this.resizerDOM.style.left = `${left}px`;
this.resizerDOM.style.width = nodeRect.width + 'px';
this.resizerDOM.style.height = nodeRect.height + 'px';
}
protected updateDOMSize(width: number, height: number): void {
if (!this.dom) {
if (!this.resizerDOM) {
return;
}
this.dom.style.width = width + 'px';
this.dom.style.height = height + 'px';
this.resizerDOM.style.width = width + 'px';
this.resizerDOM.style.height = height + 'px';
}
protected hide() {
this.mouseTracker?.teardown();
this.dom?.remove();
this.resizerDOM?.remove();
this.targetNode = null;
this.activeSelection = '';
this.loadAbortController.abort();
}
protected buildDOM() {
@@ -110,7 +134,7 @@ class NodeResizer {
}, handleElems);
}
setupTracker(container: HTMLElement, node: NodeHasSize, nodeDOM: HTMLElement): MouseDragTracker {
setupTracker(container: HTMLElement, node: NodeHasSize&LexicalNode, nodeDOM: HTMLElement): MouseDragTracker {
let startingWidth: number = 0;
let startingHeight: number = 0;
let startingRatio: number = 0;
@@ -140,7 +164,7 @@ class NodeResizer {
return new MouseDragTracker(container, '.editor-node-resizer-handle', {
down(event: MouseEvent, handle: HTMLElement) {
_this.dom?.classList.add('active');
_this.resizerDOM?.classList.add('active');
_this.context.editor.getEditorState().read(() => {
const domRect = nodeDOM.getBoundingClientRect();
startingWidth = node.getWidth() || domRect.width;
@@ -163,12 +187,15 @@ class NodeResizer {
_this.context.editor.update(() => {
node.setWidth(size.width);
node.setHeight(hasHeight ? size.height : 0);
_this.context.manager.triggerLayoutUpdate();
requestAnimationFrame(() => {
_this.updateDOMPosition(nodeDOM);
})
}, {
onUpdate: () => {
requestAnimationFrame(() => {
_this.context.manager.triggerLayoutUpdate();
_this.updateResizerPosition();
});
}
});
_this.dom?.classList.remove('active');
_this.resizerDOM?.classList.remove('active');
}
});
}

View File

@@ -15,6 +15,7 @@ class TableResizer {
protected targetCell: HTMLElement|null = null;
protected xMarkerAtStart : boolean = false;
protected yMarkerAtStart : boolean = false;
protected activeInTable: boolean = false;
constructor(editor: LexicalEditor, editScrollContainer: HTMLElement) {
this.editor = editor;
@@ -33,9 +34,10 @@ class TableResizer {
}
protected setupListeners() {
this.onTableMouseOver = this.onTableMouseOver.bind(this);
this.onCellMouseMove = this.onCellMouseMove.bind(this);
this.onScrollOrResize = this.onScrollOrResize.bind(this);
this.editScrollContainer.addEventListener('mousemove', this.onCellMouseMove);
this.editScrollContainer.addEventListener('mouseover', this.onTableMouseOver, { passive: true });
window.addEventListener('scroll', this.onScrollOrResize, {capture: true, passive: true});
window.addEventListener('resize', this.onScrollOrResize, {passive: true});
}
@@ -44,8 +46,26 @@ class TableResizer {
this.updateCurrentMarkerTargetPosition();
}
protected onTableMouseOver(event: MouseEvent): void {
if (this.dragging) {
return;
}
const table = (event.target as HTMLElement).closest('table') as HTMLElement|null;
if (table && !this.activeInTable) {
this.editScrollContainer.addEventListener('mousemove', this.onCellMouseMove, { passive: true });
this.onCellMouseMove(event);
this.activeInTable = true;
} else if (!table && this.activeInTable) {
this.editScrollContainer.removeEventListener('mousemove', this.onCellMouseMove);
this.hideMarkers();
this.activeInTable = false;
}
}
protected onCellMouseMove(event: MouseEvent) {
const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement;
const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement|null;
if (!cell || this.dragging) {
return;
}
@@ -66,10 +86,16 @@ class TableResizer {
protected updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {
const markers: MarkerDomRecord = this.getMarkers();
const table = cell.closest('table') as HTMLElement;
const caption: HTMLTableCaptionElement|null = table.querySelector('caption');
const tableRect = table.getBoundingClientRect();
const editBounds = this.editScrollContainer.getBoundingClientRect();
const maxTop = Math.max(tableRect.top, editBounds.top);
let tableTop = tableRect.top;
if (caption) {
tableTop = caption.getBoundingClientRect().bottom;
}
const maxTop = Math.max(tableTop, editBounds.top);
const maxBottom = Math.min(tableRect.bottom, editBounds.bottom);
const maxHeight = maxBottom - maxTop;
markers.x.style.left = xPos + 'px';
@@ -85,6 +111,13 @@ class TableResizer {
markers.x.hidden = tableRect.top > editBounds.bottom || tableRect.bottom < editBounds.top;
}
protected hideMarkers(): void {
if (this.markerDom) {
this.markerDom.x.hidden = true;
this.markerDom.y.hidden = true;
}
}
protected updateCurrentMarkerTargetPosition(): void {
if (!this.targetCell) {
return;

View File

@@ -34,7 +34,11 @@ export class EditorContextToolbar extends EditorContainerUiElement {
dom.hidden = !showing;
if (!showing) {
if (!this.target.isConnected) {
// If our target is no longer in the DOM, tell the manager an update is needed.
this.getContext().manager.triggerFutureStateRefresh();
return;
} else if (!showing) {
return;
}

View File

@@ -3,7 +3,7 @@ import {
getCodeToolbarContent, getDetailsToolbarContent,
getImageToolbarContent,
getLinkToolbarContent,
getMainEditorFullToolbar, getTableToolbarContent
getMainEditorFullToolbar, getMediaToolbarContent, getTableToolbarContent
} from "./defaults/toolbars";
import {EditorUIManager} from "./framework/manager";
import {EditorUiContext} from "./framework/core";
@@ -44,6 +44,10 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
selector: 'img:not([drawio-diagram] img)',
content: getImageToolbarContent(),
});
manager.registerContextToolbar('media', {
selector: '.editor-media-wrap',
content: getMediaToolbarContent(),
});
manager.registerContextToolbar('link', {
selector: 'a',
content: getLinkToolbarContent(),

View File

@@ -52,12 +52,19 @@ export type StyleMap = Map<string, string>;
/**
* Creates a map from an element's styles.
* Uses direct attribute value string handling since attempting to iterate
* over .style will expand out any shorthand properties (like 'padding') making
* over .style will expand out any shorthand properties (like 'padding')
* rather than being representative of the actual properties set.
*/
export function extractStyleMapFromElement(element: HTMLElement): StyleMap {
const map: StyleMap = new Map();
const styleText= element.getAttribute('style') || '';
return styleStringToStyleMap(styleText);
}
/**
* Convert string-formatted styles into a StyleMap.
*/
export function styleStringToStyleMap(styleText: string): StyleMap {
const map: StyleMap = new Map();
const rules = styleText.split(';');
for (const rule of rules) {
@@ -72,6 +79,17 @@ export function extractStyleMapFromElement(element: HTMLElement): StyleMap {
return map;
}
/**
* Convert a StyleMap into inline string style text.
*/
export function styleMapToStyleString(map: StyleMap): string {
const parts = [];
for (const [style, value] of map.entries()) {
parts.push(`${style}:${value}`);
}
return parts.join(';');
}
export function setOrRemoveAttribute(element: HTMLElement, name: string, value: string|null|undefined) {
if (value) {
element.setAttribute(name, value);

View File

@@ -3,7 +3,7 @@ import {
$createParagraphNode, $createRangeSelection,
$getRoot,
$getSelection, $isBlockElementNode, $isDecoratorNode,
$isElementNode,
$isElementNode, $isParagraphNode,
$isTextNode,
$setSelection,
BaseSelection, DecoratorNode,
@@ -60,12 +60,19 @@ export function $selectionContainsTextFormat(selection: BaseSelection | null, fo
return false;
}
for (const node of selection.getNodes()) {
// Check text nodes
const nodes = selection.getNodes();
for (const node of nodes) {
if ($isTextNode(node) && node.hasFormat(format)) {
return true;
}
}
// If we're in an empty paragraph, check the paragraph format
if (nodes.length === 1 && $isParagraphNode(nodes[0]) && nodes[0].hasTextFormat(format)) {
return true;
}
return false;
}

View File

@@ -454,7 +454,7 @@ body.editor-is-fullscreen {
.editor-media-wrap {
display: inline-block;
cursor: not-allowed;
iframe {
iframe, video {
pointer-events: none;
}
&.align-left {

View File

@@ -106,4 +106,14 @@ class SearchIndexingTest extends TestCase
$this->assertNull($scoreByTerm->get($term), "Failed asserting that \"$term\" is not indexed");
}
}
public function test_non_breaking_spaces_handled_as_spaces()
{
$page = $this->entities->newPage(['html' => '<p>a&nbsp;tigerbadger is a dangerous&nbsp;animal</p>']);
$scoreByTerm = $page->searchTerms()->pluck('score', 'term');
$this->assertNotNull($scoreByTerm->get('tigerbadger'));
$this->assertNotNull($scoreByTerm->get('dangerous'));
$this->assertNotNull($scoreByTerm->get('animal'));
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Tests\Uploads;
use BookStack\Exceptions\DrawioPngReaderException;
use BookStack\Uploads\DrawioPngReader;
use Tests\TestCase;
class DrawioPngReaderTest extends TestCase
{
public function test_exact_drawing()
{
$file = $this->files->testFilePath('test.drawio.png');
$stream = fopen($file, 'r');
$reader = new DrawioPngReader($stream);
$drawing = $reader->extractDrawing();
$this->assertStringStartsWith('<mxfile ', $drawing);
$this->assertStringEndsWith("</mxfile>\n", $drawing);
}
public function test_extract_drawing_with_non_drawing_image_throws_exception()
{
$file = $this->files->testFilePath('test-image.png');
$stream = fopen($file, 'r');
$reader = new DrawioPngReader($stream);
$exception = null;
try {
$drawing = $reader->extractDrawing();
} catch (\Exception $e) {
$exception = $e;
}
$this->assertInstanceOf(DrawioPngReaderException::class, $exception);
$this->assertEquals($exception->getMessage(), 'Unable to find drawing data within PNG file');
}
public function test_extract_drawing_with_non_png_image_throws_exception()
{
$file = $this->files->testFilePath('test-image.jpg');
$stream = fopen($file, 'r');
$reader = new DrawioPngReader($stream);
$exception = null;
try {
$drawing = $reader->extractDrawing();
} catch (\Exception $e) {
$exception = $e;
}
$this->assertInstanceOf(DrawioPngReaderException::class, $exception);
$this->assertEquals($exception->getMessage(), 'File does not appear to be a valid PNG file');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB