Compare commits

...

108 Commits

Author SHA1 Message Date
Dan Brown
bf0ba9f756 Replaced embeds with images in exports 2022-05-24 18:05:47 +01:00
Dan Brown
05f8034439 Added embed support for contained HTML exports
Unfortunately CSP rules will block embeds anyway.
Need to either relax CSP rules on exports, or instead convert to img
tags?

Also cleaned up existing regexes.
2022-05-23 16:11:28 +01:00
Dan Brown
1d1186c901 Replaced markdown preview display iframe with div
No longer need to use the iframe sandboxing techniques, since we now have
CSP rules in-place. Means that embed tags can load without CSP
complications.

Causes slight change to contents of `editor-markdown::setup` editor
event data, since we're changing the `displayEl` property.

Updates markdown editor component to make better use of the component
system.
2022-05-23 15:16:23 +01:00
Dan Brown
641a26cdf7 Updated markdown editor to use svg drawio images
- Also tweaked page editor to not error when the current user does not
  have permission to change editor type.
2022-05-23 14:38:34 +01:00
Dan Brown
5fd8e7e0e9 Updated drawio tinymce plugin to use embeds
- Adds support for handling drawings as embeds, based on image
  extension.
- Adds additional attribute to drawio elements within editor to prevent
  tinymce replacing embeds with a placeholder.
- Updates how contenteditable is applied to drawio blocks within editor,
  to use proper filters instead of using the SetContent event.
2022-05-23 12:24:40 +01:00
Dan Brown
d926ca5f71 Updated draw.io code to support SVGs as primary data type 2022-05-22 12:38:50 +01:00
Dan Brown
b69722c3b5 Fixed issue caused by changing test method defaults 2022-05-22 11:58:22 +01:00
Dan Brown
c9aa1c979f Added SVG support to the image gallery. 2022-05-22 11:52:42 +01:00
Dan Brown
49498cfaf9 Fixed entity-specific tag counts listing
Was reporting wrong due to use of old polymorphic namespace references.
Test was not picking up as assertElementContains had wider scope than
expected, looking within the HTML of the element instead of the text
which you might expect. Updated test helper to look at text instead.
2022-05-16 14:05:21 +01:00
Dan Brown
3a4aa81115 Removed dialog debug script from default home
Accidentally left in from before.
Closes #3430
2022-05-16 13:36:42 +01:00
Dan Brown
f021823287 Updated default value for secure session detection
Updated default value for APP_URL so that the startsWith call is not
passed null, since that causes deprecation notice in PHP8.1.
Would show when APP_URL was not set, adding extra confusiion.
2022-05-11 16:47:09 +01:00
Dan Brown
3a8a476906 Updated translators, applied styleCI change 2022-05-09 16:09:31 +01:00
Dan Brown
328bc88f02 Fixed LDAP_DUMP_* options when data contains binary
Dumping details that were binary, such as the jpegphoto data, would
cause the dump to fail on the encoding to JSON.
This change forces content to be UTF8 before dumping.
Updated existing test to cover.

Closes #3396
2022-05-09 15:57:50 +01:00
Dan Brown
2a99e23e6d Updated attachment download to check OB before cleaning it
Call to `ob_end_clean` would error if the environment did not use the
PHP `output_buffering` option. This adds an additional check and updates
the comment to be more specific to the exact scenario of the condition.
Tested with output_buffering=Off and output_buffering=4096

Closes #3415
2022-05-09 15:25:06 +01:00
Dan Brown
b855bbaaea New Crowdin updates (#3418) 2022-05-09 15:15:35 +01:00
Dan Brown
96436839f1 Added rate limit section to the API docs
Closes #3423
2022-05-09 15:12:29 +01:00
Dan Brown
b4f29a85ab Added Farsi language available
Closes #3426
2022-05-09 14:58:04 +01:00
Dan Brown
4a2a044f3d Updated PHP deps 2022-05-09 14:57:34 +01:00
Dan Brown
ca09ed916f Added support plans link to issue links 2022-05-05 15:48:27 +01:00
Dan Brown
dbefda055f Updated method of string interpolation
In prep for future PHP changes as per RFC
https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation
2022-05-05 09:33:25 +01:00
Dan Brown
93ef8c97b6 Applied styleci changes 2022-05-04 21:19:46 +01:00
Dan Brown
420b29f32f New Crowdin updates (#3402) 2022-05-04 21:18:47 +01:00
Dan Brown
d2ed98d20d Merge branch 'development' of github.com:BookStackApp/BookStack into development 2022-05-04 21:01:20 +01:00
Dan Brown
ebc69a8f2c Fixed double path slash URL issue in some cases
- Occurred on system request path usage (Primarily on guest login
  redirection) when a custom path was not in use.
- Added test to cover.

For #3404
2022-05-04 20:08:22 +01:00
Dan Brown
44013721f0 New Crowdin updates (#3401) 2022-04-29 15:53:06 +01:00
Dan Brown
16222de5fa Added uzbeck into local list
Not yet an actual added language yet due to low translation rate.
2022-04-29 15:52:11 +01:00
Dan Brown
ebfe946160 Updated translation attribution before v22.04 2022-04-29 15:43:30 +01:00
Dan Brown
5d2aad6a9e Merge pull request #3373 from evandroamaro/patch-1
Tiny header
2022-04-29 15:41:04 +01:00
Dan Brown
8fb016d1bf New Crowdin updates (#3384) 2022-04-29 15:40:38 +01:00
Dan Brown
c216a6a210 Applied stylci changes, updated composer deps 2022-04-29 15:38:06 +01:00
Dan Brown
26af9acc6c Improved iframe & summary handling in HTML to MD conversion 2022-04-29 14:58:28 +01:00
Dan Brown
c8a7acb6c7 Fixed drawing handling on HTML to Markdown conversion 2022-04-29 12:17:14 +01:00
Dan Brown
d3b39fbe50 Move html to markdown formatting tests to their own class 2022-04-29 11:50:34 +01:00
Dan Brown
ac7b2dd1bf Tweaked DRAW.IO params in complete .env file to show configure param 2022-04-27 17:52:35 +01:00
Dan Brown
f1a8ad4980 Applied latest StyleCI changes 2022-04-25 18:42:31 +01:00
Dan Brown
d5b7fff102 Merge branch 'recycle_bin_api_endpoints' into development 2022-04-25 18:32:55 +01:00
Dan Brown
0930e8519c Updated polymorphic database relation types to simpler version
- Means we can use these simpler types in API response, As desired in #3377.

Closes #3395
2022-04-25 18:31:37 +01:00
Dan Brown
ff8dadefee Reviewed recycle bin API PR and made changes
Made the following changes, many of these are just to align with
existing conventions.

- Updated urls to be hypenated, instead of underscored, to match other system endpoints.
- Updated URL parameter to be `deletionId` instead of `id`, and removed the ID-based comment on controller methods, so the required ID model is clear from the URL alone, since its not clear from the URL endpoint alone like existing endpoints. This follows the pattern used in the "web" routes.
- Added extra detail on some controller method comments, and copied permission comment to each method.
- Removed existing field visibility mechanisms to use simpler model-based visibility since we didn't need anything too special here (After some of my other changes).
- Allowed the "deletable" model to be shown in response to provide a little more detail on the main deleted item.
- Updated parent/child-count loading to be on the "deletable" model instead of additional properties which results in simpler controller logic and enforces the idea these are relations on the deletable, not the deletion itself. It also removes additional exposure of model namespacing.
- Updated (int) casts to intval, just since that's our most common conversion method in the codebase.
- Testing: Removed `actingAsAuthorizedUser` and used the admin user instead to prevent extra auth steps on each test.
- Testing: Cut logic/data-checks from tests if already covered by other tests.
- Testing: Added simple assertions for delete/restore response data.
- Examples: Updated list example to reflect changes.

Review of PR #3377
To be followed up with changes to polymorphic relations to hide
namespacing.
2022-04-25 17:54:59 +01:00
Dan Brown
2b0ae23da0 Updated composer deps, applied latest StyleCI changes 2022-04-24 18:22:40 +01:00
Dan Brown
63cb6015a8 Merge pull request #3364 from BookStackApp/app_url_requests
Updated custom request overrides to better match original intent
2022-04-24 14:52:38 +01:00
Dan Brown
5a7fb20116 Merge pull request #3387 from BookStackApp/editor_switching
Page editor switching
2022-04-24 14:03:03 +01:00
Dan Brown
829f808800 Merge pull request #3365 from BookStackApp/data_streaming
Add data streaming where beneficial to reduce memory usage
2022-04-24 13:59:47 +01:00
Dan Brown
0dfe5cb66b Merge pull request #3391 from BookStackApp/drawio_config_event
Made it possible to configure draw.io/diagrams.net integration
2022-04-24 13:58:59 +01:00
julesdevops
14bccae6bd do some cleanup and add doc 2022-04-24 10:49:29 +02:00
Dan Brown
b97c150ac8 Added additional testing for editor switching permissions 2022-04-23 23:34:15 +01:00
Dan Brown
0c5723d76e Switched to database-based tracking for page editor
- Works better to avoid bad assumptions when showing the editor based
  upon content type.
- Also updated some previous tests to cleaner format.
2022-04-23 23:20:46 +01:00
Dan Brown
bec61a56c0 Added listing of editor type to revisions
- Also tweaked some editor revision table styles and merged some
  sections to reduce space usage.
2022-04-23 15:03:58 +01:00
Dan Brown
1b46aa8756 Aded tests for core editor switching functionality 2022-04-23 14:22:04 +01:00
julesdevops
f14e6e8f2d Complete list endpoint and add some tests 2022-04-21 22:23:24 +02:00
Dan Brown
0003ce61cd Fixed failing test after drawio default url change 2022-04-20 23:42:47 +01:00
Dan Brown
d76bbb2954 Made it possible to configure draw.io/diagrams.net integration
Added new editor public event to hook into draw.io configuration step.
Required change of embed url to trigger the configure step.
2022-04-20 23:32:02 +01:00
Dan Brown
478067483f Linked up confirmation prompt to editor switching 2022-04-20 18:21:21 +01:00
Dan Brown
eff539f89b Added new confirm-dialog component, both view and logic 2022-04-20 14:58:37 +01:00
Dan Brown
214992650d Standardised dropdown list item styles, Extracted page editor toolbar
- Updated all dropdown list item actions into three specific styles:
  icon-item, text-item & label-item. Allows a stronger structure while
  prevents mixing of styles as we were getting for header dropdown in
  dark mode.
- Extracted out page editor top toolbar to its own view file & split
  editor switch options to different markdown options.
2022-04-20 14:03:47 +01:00
Dan Brown
492ffff0a4 Added core editor switching functionality 2022-04-18 17:39:28 +01:00
Dan Brown
956eb1308f Aligned page edit controller method data usage
Extracted page editor view data gathering to its own class for
alignment. Updated the data used in views as part of the process to use
view-specific variables instead of custom attributes added to models.
Also moved tinymce library loading so it's not loaded when not using the
wysiwyg editor.
2022-04-17 23:01:14 +01:00
Dan Brown
0cc215f8c3 Added editor type change button 2022-04-17 15:01:29 +01:00
Dan Brown
e8e38f1f7b Added an 'editor-change' role permission 2022-04-17 14:33:06 +01:00
Dan Brown
7dc80a9e14 Updated editor setting to reflect "Default editor" 2022-04-17 14:13:14 +01:00
Dan Brown
e49afdbd72 New Crowdin updates (#3358) 2022-04-14 16:14:05 +01:00
Dan Brown
56254bdb66 Added testing for our request method overrides 2022-04-13 13:02:42 +01:00
Dan Brown
25654b2322 Fixed base URL starting slash usage 2022-04-13 12:46:19 +01:00
Dan Brown
27339079f7 Extracted esbuild config to a build script
Allows us to use NodeJS code for file/directory locating to not be
shell/os specific, while also also reducing duplicated complexity within
packages.json file.

Related to #3323
2022-04-13 12:08:56 +01:00
julesdevops
55e52e45fb Start recycle bin API endpoints: list, restore, delete 2022-04-07 22:34:00 +02:00
evandroamaro
c979e6465e Tiny header
Had the same translation as the small header. Corrected the translation.
2022-04-05 10:53:52 +01:00
Dan Brown
c30a9d3564 Touched entity timestamps on entity tag update
Decided it's relevant to entity updated_at since tags are now indexed
alongside content.

- Also fixed tags not applied on shelf.
- Also enforced proper page API update validation.
- Adds tests to cover.

For #3319
Fixes #3370
2022-04-04 17:24:05 +01:00
Dan Brown
59d1fb2d10 Fixed tests from streaming changes
- Added testing check to buffer stop/clear on streaming output due to
  interference during tests.
- Made content-disposition header a little safer in download responses.
- Also aligned how we check for testing environment.
2022-04-03 16:22:31 +01:00
Dan Brown
08a8c0070e Added streaming support to API attachment read responses
Required some special handling due to the content being base64-encoded
within a JSON response.
2022-04-02 19:21:19 +01:00
Dan Brown
cb770c534d Added streamed uploads for attachments 2022-04-02 18:46:48 +01:00
Dan Brown
6749faa89a Fixed streamed outputs in more extreme scenarios
Fixes hitting memory limits where downloaded file sizes are much greater
than memory limit. Stopping and flushing output buffer seemed to stop
limits causing issues when fpassthru is used.
Tested with 24M memory limit and 734M file
2022-04-02 18:42:15 +01:00
Dan Brown
82e8b1577e Updated attachment download responses to stream from filesystem
This allows download of attachments that are larger than current memory
limits, since we're not loading the entire file into memory any more.

For inline file responses, we take a 1kb portion of the file to sniff
before to check mime before we proceed.
2022-04-02 18:07:43 +01:00
Dan Brown
4dce03c0d3 Updated custom request overrides to better match original intent
This updates the custom Request handler to provide only the scheme and
host on the `getSchemeAndHttpHost` call, instead of providing the whole
APP_URL value, while adding an override to the 'getBaseUrl' to use the
APP_URL content instead of the guessed/detected Symfony value.

Untested apart from simple local setup.

Related to #2765
2022-04-02 17:14:37 +01:00
Dan Brown
affae2e3c4 New Crowdin updates (#3354) 2022-03-30 19:29:13 +01:00
Dan Brown
1a90b98b8f Updated composer dependancies 2022-03-30 19:22:47 +01:00
Dan Brown
da4308bb0f Fixed settings redirect issue and custom head display
- Fixed issue where redirect for `/settings` view would not be ran
  through base url generator so would not create a correct path in some
  cases. Now routed through controller with normal redirect.
- Fixed custom head content being active on settings pages due to route
  name changes, for when viewing settings, in last release.

Fixes #3356 and #3355
2022-03-30 19:15:24 +01:00
Dan Brown
135022136a New Crowdin updates (#3353) 2022-03-30 13:31:59 +01:00
Dan Brown
12f96bb1a4 Updated translation contributors, added Basque to language options 2022-03-30 13:12:17 +01:00
Dan Brown
678314a0c5 New Crowdin updates (#3320) 2022-03-30 13:00:27 +01:00
Dan Brown
0887c39694 Updated example env with LDAP group dump option 2022-03-29 11:49:02 +01:00
Dan Brown
078e8e7dc3 PHPStan and StyleCI fixes
- Updated PhpStan PHP version option to match project.
- Applied StyleCI changes.
- Updated static to self in WebhookFormatter, following static analysis
  guidance.
- Fixed mis-matched header tags.
2022-03-28 11:31:06 +01:00
Dan Brown
038015f852 Merge pull request #3349 from BookStackApp/settings_reorg
Reorganization of settings view
2022-03-28 11:22:21 +01:00
Dan Brown
7c12920dc8 Added 404 response for non-existing setting categories
- Added test to cover.
2022-03-28 11:16:20 +01:00
Dan Brown
895f656897 Split out settings view and made functional
- Split settings out to new views using a core shared layout.
- Extracted added language text to translation files.
- Updated settings routes to be dynamic to category.
- Added redirect for old primary settings route.
- Updated existing tests to cover settings route changes.
- Added tests to cover settings view.
- Improved contrast of settings links for dark mode.
2022-03-28 11:09:55 +01:00
Dan Brown
31dbf132b9 Started playing with new settings view layout 2022-03-26 21:36:05 +00:00
Dan Brown
b5281bc9ca Fixed tests, applied StyleCI changes 2022-03-26 20:38:03 +00:00
Dan Brown
3625f12abe Added extendable/scalable formatter for webhook data
Creates a new organsied formatting system for webhook data, with
interfaces for extending with custom model formatting rules.
Allows easy usage & extension of the default bookstack formatting
behaviour when customizing webhook events via theme system, and keeps
default data customizations organised.

This also makes the following webhook data changes:
- owned_by/created_by/updated_by user details are loaded for events with
  Entity details. (POTENTIALLY BREAKING CHANGE).
- current_revision details are loaded for page update/create events.

Added testing to cover added model formatting rules.

For #3279 and #3218
2022-03-26 16:53:02 +00:00
Dan Brown
55d61fceb2 Added manual image thumbnail exif orientation handling
Uses original image data to extract orientation exif to apply image
transformations before scaling and save. Manually done due to issues
with exif data loss during the existing Invervention image path.

For #1854
2022-03-26 12:32:08 +00:00
Dan Brown
2325a307a5 Applied latest styleCI changes 2022-03-25 11:14:27 +00:00
Dan Brown
d2b49084b0 Added pre-render sizes to wysiwyg code blocks
Sets sizes on WYSIWYG code block sections based on content lines
as an early pre-codemirror height prediction to avoid excessive
jumping in the editor.

For #3326
2022-03-25 11:13:04 +00:00
Dan Brown
8594f42584 Added LDAP group debugging env option
Closes #3345
2022-03-23 16:34:23 +00:00
Dan Brown
dd7463259a Added wysiwyg filter to handle <br> tags within code blocks
This filters out <br> elements within code blocks and replaces them with
newlines. The editor started using <br>'s more harshley after some
configuration changes upon upgrading tinymce, in which we standardised
on forced br tags to avoid empty elements.

For #3327
2022-03-23 15:11:14 +00:00
Dan Brown
d23b24b8db Added additional missing editor translations
- Also merged StyleCI fixes

As per #3342
2022-03-23 14:41:54 +00:00
Dan Brown
1c859e94e0 Fixed conctenation of direct book pages within markdown export
- Updated to ensure seperation with newlines.
- Added test to cover.

For #3341
2022-03-23 14:31:42 +00:00
Dan Brown
981807220c Applied StyleCI changes and updated dependancies 2022-03-23 12:02:01 +00:00
Dan Brown
a2231c3604 Merge pull request #3333 from BookStackApp/wysiwyg_tasklist
WYSIWYG tasklist support
2022-03-23 11:58:16 +00:00
Dan Brown
622adc5450 Updated justify translation for editor
Fixes #3342
2022-03-23 11:57:20 +00:00
Dan Brown
95e496d16f Added translation string for tasklist WYSIWYG action 2022-03-23 11:54:27 +00:00
Dan Brown
883e18f7c4 Updated tasklist style and functionality for cross-browser use
- Updated styles to better align checkboxes within page content.
- Updated functionality to use a cross-compatible property on checkbox
  click within the editor.
2022-03-23 11:51:19 +00:00
Dan Brown
c5aad29c72 Added tasklist support to markdown exporter 2022-03-22 14:56:51 +00:00
Dan Brown
ea62fe6004 Improved tasklist wysiwyg behaviour
- Updated buttons/actions to better handle nesting.
- Added hack for better usage with normal bullets
2022-03-22 14:03:20 +00:00
Dan Brown
5ae9ed1e22 Added functioning wysiwyg tasklist toolbar button
- Includes new icon.
- Includes menu button overrides of existing list styles to prevent
  incompatible mixing.
2022-03-20 13:30:48 +00:00
Dan Brown
b6be8a2bb9 Added WYSIWYG tasklist clicking ability 2022-03-20 11:59:46 +00:00
Dan Brown
65dd7ad1e9 Changed to a psuedo-style approach for tasklist in wysiwyg 2022-03-19 17:13:26 +00:00
Dan Brown
f991948c49 Started initial tasklist attempt, failed implementation 2022-03-19 16:04:33 +00:00
Dan Brown
ee6a2339b6 Applied latest styleCI changes 2022-03-09 14:30:36 +00:00
Dan Brown
fd26f54b99 Merge pull request #3298 from BookStackApp/wysiwyg_links
WYSIWYG editor link updates
2022-03-09 14:29:03 +00:00
Dan Brown
6252b46395 Added a custom link context toolbar
- Allows for easy unlinking, link preview or link editing.
- Created custom one to limit actions available.
- Performed refactoring of non-plugin toolbar editor code to extact into
  its own file.

Related to #3276
2022-02-28 13:56:23 +00:00
Dan Brown
20ecaa5c5a Added ctrl+shift+k shortcut to WYSIWYG
Shows entity select dialog for more direct entity link insertion.
Aligns with shortcut from markdown editor.

For #3244
2022-02-28 13:34:32 +00:00
321 changed files with 9262 additions and 3074 deletions

View File

@@ -223,6 +223,7 @@ LDAP_DUMP_USER_DETAILS=false
LDAP_USER_TO_GROUPS=false
LDAP_GROUP_ATTRIBUTE="memberOf"
LDAP_REMOVE_FROM_GROUPS=false
LDAP_DUMP_USER_GROUPS=false
# SAML authentication configuration
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
@@ -273,7 +274,7 @@ AVATAR_URL=
# Enable diagrams.net integration
# Can simply be true/false to enable/disable the integration.
# Alternatively, It can be URL to the diagrams.net instance you want to use.
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1&configure=1
DRAWIO=true
# Default item listing view

View File

@@ -1,9 +1,13 @@
blank_issues_enabled: false
contact_links:
- name: Discord chat support
- name: Discord Chat Support
url: https://discord.gg/ztkBqR2
about: Realtime support / chat with the community and the team.
about: Realtime support & chat with the BookStack community and the team.
- name: Debugging & Common Issues
url: https://www.bookstackapp.com/docs/admin/debugging/
about: Find details on how to debug issues and view common issues with thier resolutions.
about: Find details on how to debug issues and view common issues with their resolutions.
- name: Official Support Plans
url: https://www.bookstackapp.com/support/
about: View our official support plans that offer assured support for business.

View File

@@ -165,7 +165,7 @@ Francesco Franchina (ffranchina) :: Italian
Aimrane Kds (aimrane.kds) :: Arabic
whenwesober :: Indonesian
Rem (remkovdhoef) :: Dutch
syn7ax69 :: Bulgarian; Turkish
syn7ax69 :: Bulgarian; Turkish; German
Blaade :: French
Behzad HosseinPoor (behzad.hp) :: Persian
Ole Aldric (Swoy) :: Norwegian Bokmal
@@ -232,3 +232,14 @@ msevgen :: Turkish
Khroners :: French
MASOUD HOSSEINY (masoudme) :: Persian
Thomerson Roncally (roncallyt) :: Portuguese, Brazilian
metaarch :: Bulgarian
Xabi (xabikip) :: Basque
pedromcsousa :: Portuguese
Nir Louk (looknear) :: Hebrew
Alex (qianmengnet) :: Chinese Simplified
stothew :: German
sgenc :: Turkish
Shukrullo (vodiylik) :: Uzbek
William W. (Nevnt) :: Chinese Traditional
eamaro :: Portuguese
Ypsilon-dev :: Arabic

View File

@@ -3,17 +3,14 @@
namespace BookStack\Actions;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Theme;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use BookStack\Theming\ThemeEvents;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -24,31 +21,16 @@ class DispatchWebhookJob implements ShouldQueue
use Queueable;
use SerializesModels;
/**
* @var Webhook
*/
protected $webhook;
/**
* @var string
*/
protected $event;
protected Webhook $webhook;
protected string $event;
protected User $initiator;
protected int $initiatedTime;
/**
* @var string|Loggable
*/
protected $detail;
/**
* @var User
*/
protected $initiator;
/**
* @var int
*/
protected $initiatedTime;
/**
* Create a new job instance.
*
@@ -70,8 +52,8 @@ class DispatchWebhookJob implements ShouldQueue
*/
public function handle()
{
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail);
$webhookData = $themeResponse ?? $this->buildWebhookData();
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime);
$webhookData = $themeResponse ?? WebhookFormatter::getDefault($this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime)->format();
$lastError = null;
try {
@@ -97,36 +79,4 @@ class DispatchWebhookJob implements ShouldQueue
$this->webhook->save();
}
protected function buildWebhookData(): array
{
$textParts = [
$this->initiator->name,
trans('activities.' . $this->event),
];
if ($this->detail instanceof Entity) {
$textParts[] = '"' . $this->detail->name . '"';
}
$data = [
'event' => $this->event,
'text' => implode(' ', $textParts),
'triggered_at' => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),
'triggered_by' => $this->initiator->attributesToArray(),
'triggered_by_profile_url' => $this->initiator->getProfileUrl(),
'webhook_id' => $this->webhook->id,
'webhook_name' => $this->webhook->name,
];
if (method_exists($this->detail, 'getUrl')) {
$data['url'] = $this->detail->getUrl();
}
if ($this->detail instanceof Model) {
$data['related_item'] = $this->detail->attributesToArray();
}
return $data;
}
}

View File

@@ -28,10 +28,10 @@ class TagRepo
'name',
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
DB::raw('COUNT(id) as usages'),
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Page\', 1, 0)) as page_count'),
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Chapter\', 1, 0)) as chapter_count'),
DB::raw('SUM(IF(entity_type = \'BookStack\\\\Book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'BookStack\\\\BookShelf\', 1, 0)) as shelf_count'),
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
])
->orderBy($nameFilter ? 'value' : 'name');

View File

@@ -0,0 +1,124 @@
<?php
namespace BookStack\Actions;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Support\Carbon;
class WebhookFormatter
{
protected Webhook $webhook;
protected string $event;
protected User $initiator;
protected int $initiatedTime;
/**
* @var string|Loggable
*/
protected $detail;
/**
* @var array{condition: callable(string, Model):bool, format: callable(Model):void}[]
*/
protected $modelFormatters = [];
public function __construct(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime)
{
$this->webhook = $webhook;
$this->event = $event;
$this->initiator = $initiator;
$this->initiatedTime = $initiatedTime;
$this->detail = is_object($detail) ? clone $detail : $detail;
}
public function format(): array
{
$data = [
'event' => $this->event,
'text' => $this->formatText(),
'triggered_at' => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),
'triggered_by' => $this->initiator->attributesToArray(),
'triggered_by_profile_url' => $this->initiator->getProfileUrl(),
'webhook_id' => $this->webhook->id,
'webhook_name' => $this->webhook->name,
];
if (method_exists($this->detail, 'getUrl')) {
$data['url'] = $this->detail->getUrl();
}
if ($this->detail instanceof Model) {
$data['related_item'] = $this->formatModel();
}
return $data;
}
/**
* @param callable(string, Model):bool $condition
* @param callable(Model):void $format
*/
public function addModelFormatter(callable $condition, callable $format): void
{
$this->modelFormatters[] = [
'condition' => $condition,
'format' => $format,
];
}
public function addDefaultModelFormatters(): void
{
// Load entity owner, creator, updater details
$this->addModelFormatter(
fn ($event, $model) => ($model instanceof Entity),
fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy'])
);
// Load revision detail for page update and create events
$this->addModelFormatter(
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
fn ($model) => $model->load('currentRevision')
);
}
protected function formatModel(): array
{
/** @var Model $model */
$model = $this->detail;
$model->unsetRelations();
foreach ($this->modelFormatters as $formatter) {
if ($formatter['condition']($this->event, $model)) {
$formatter['format']($model);
}
}
return $model->toArray();
}
protected function formatText(): string
{
$textParts = [
$this->initiator->name,
trans('activities.' . $this->event),
];
if ($this->detail instanceof Entity) {
$textParts[] = '"' . $this->detail->name . '"';
}
return implode(' ', $textParts);
}
public static function getDefault(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime): self
{
$instance = new self($event, $webhook, $detail, $initiator, $initiatedTime);
$instance->addDefaultModelFormatters();
return $instance;
}
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Auth\Access\Guards;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
@@ -15,7 +16,7 @@ use Illuminate\Support\Str;
class LdapSessionGuard extends ExternalBaseSessionGuard
{
protected $ldapService;
protected LdapService $ldapService;
/**
* LdapSessionGuard constructor.
@@ -59,8 +60,9 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
* @param array $credentials
* @param bool $remember
*
* @throws LdapException*@throws \BookStack\Exceptions\JsonDebugException
* @throws LoginAttemptException
* @throws LdapException
* @throws JsonDebugException
*
* @return bool
*/

View File

@@ -15,12 +15,17 @@ use Illuminate\Support\Facades\Log;
*/
class LdapService
{
protected $ldap;
protected $groupSyncService;
protected Ldap $ldap;
protected GroupSyncService $groupSyncService;
protected UserAvatars $userAvatars;
/**
* @var resource
*/
protected $ldapConnection;
protected $userAvatars;
protected $config;
protected $enabled;
protected array $config;
protected bool $enabled;
/**
* LdapService constructor.
@@ -274,6 +279,7 @@ class LdapService
* Get the groups a user is a part of on ldap.
*
* @throws LdapException
* @throws JsonDebugException
*/
public function getUserGroups(string $userName): array
{
@@ -285,8 +291,17 @@ class LdapService
}
$userGroups = $this->groupFilter($user);
$allGroups = $this->getGroupsRecursive($userGroups, []);
return $this->getGroupsRecursive($userGroups, []);
if ($this->config['dump_user_groups']) {
throw new JsonDebugException([
'details_from_ldap' => $user,
'parsed_direct_user_groups' => $userGroups,
'parsed_recursive_user_groups' => $allGroups,
]);
}
return $allGroups;
}
/**
@@ -369,6 +384,7 @@ class LdapService
* Sync the LDAP groups to the user roles for the current user.
*
* @throws LdapException
* @throws JsonDebugException
*/
public function syncGroups(User $user, string $username)
{

View File

@@ -71,7 +71,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'],
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
// Application Fallback Locale
'fallback_locale' => 'en',

View File

@@ -119,6 +119,7 @@ return [
'ldap' => [
'server' => env('LDAP_SERVER', false),
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
'dump_user_groups' => env('LDAP_DUMP_USER_GROUPS', false),
'dn' => env('LDAP_DN', false),
'pass' => env('LDAP_PASS', false),
'base_dn' => env('LDAP_BASE_DN', false),

View File

@@ -72,7 +72,7 @@ return [
// to the server if the browser has a HTTPS connection. This will keep
// the cookie from being sent to you if it can not be done securely.
'secure' => env('SESSION_SECURE_COOKIE', null)
?? Str::startsWith(env('APP_URL'), 'https:'),
?? Str::startsWith(env('APP_URL', ''), 'https:'),
// HTTP Access Only
// Setting this value to true will prevent JavaScript from accessing the

View File

@@ -10,10 +10,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
* @property int $deleted_by
* @property string $deletable_type
* @property int $deletable_id
* @property Deletable $deletable
*/
class Deletion extends Model implements Loggable
{
protected $hidden = [];
/**
* Get the related deletable record.
*/

View File

@@ -10,19 +10,23 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* Class Page.
*
* @property int $chapter_id
* @property string $html
* @property string $markdown
* @property string $text
* @property bool $template
* @property bool $draft
* @property int $revision_count
* @property Chapter $chapter
* @property Collection $attachments
* @property int $chapter_id
* @property string $html
* @property string $markdown
* @property string $text
* @property bool $template
* @property bool $draft
* @property int $revision_count
* @property string $editor
* @property Chapter $chapter
* @property Collection $attachments
* @property Collection $revisions
* @property PageRevision $currentRevision
*/
class Page extends BookChild
{
@@ -82,6 +86,19 @@ class Page extends BookChild
->orderBy('id', 'desc');
}
/**
* Get the current revision for the page if existing.
*
* @return PageRevision|null
*/
public function currentRevision(): HasOne
{
return $this->hasOne(PageRevision::class)
->where('type', '=', 'version')
->orderBy('created_at', 'desc')
->orderBy('id', 'desc');
}
/**
* Get all revision instances assigned to this page.
* Includes all types of revisions.
@@ -117,16 +134,6 @@ class Page extends BookChild
return url('/' . implode('/', $parts));
}
/**
* Get the current revision for the page if existing.
*
* @return PageRevision|null
*/
public function getCurrentRevision()
{
return $this->revisions()->first();
}
/**
* Get this page for JSON display.
*/

View File

@@ -10,7 +10,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class PageRevision.
*
* @property mixed $id
* @property int $page_id
* @property string $name
* @property string $slug
* @property string $book_slug
* @property int $created_by
@@ -20,13 +22,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $summary
* @property string $markdown
* @property string $html
* @property string $text
* @property int $revision_number
* @property Page $page
* @property-read ?User $createdBy
*/
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
protected $fillable = ['name', 'text', 'summary'];
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
/**
* Get the user that created the page revision.

View File

@@ -11,8 +11,8 @@ use Illuminate\Http\UploadedFile;
class BaseRepo
{
protected $tagRepo;
protected $imageRepo;
protected TagRepo $tagRepo;
protected ImageRepo $imageRepo;
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
{
@@ -58,6 +58,7 @@ class BaseRepo
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entity, $input['tags']);
$entity->touch();
}
$entity->rebuildPermissions();

View File

@@ -0,0 +1,36 @@
<?php
namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Facades\Activity;
class DeletionRepo
{
private TrashCan $trashCan;
public function __construct(TrashCan $trashCan)
{
$this->trashCan = $trashCan;
}
public function restore(int $id): int
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
return $this->trashCan->restoreFromDeletion($deletion);
}
public function destroy(int $id): int
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
return $this->trashCan->destroyFromDeletion($deletion);
}
}

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
@@ -217,11 +218,25 @@ class PageRepo
}
$pageContent = new PageContent($page);
if (!empty($input['markdown'] ?? '')) {
$currentEditor = $page->editor ?: PageEditorData::getSystemDefaultEditor();
$newEditor = $currentEditor;
$haveInput = isset($input['markdown']) || isset($input['html']);
$inputEmpty = empty($input['markdown']) && empty($input['html']);
if ($haveInput && $inputEmpty) {
$pageContent->setNewHTML('');
} elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
$newEditor = 'markdown';
$pageContent->setNewMarkdown($input['markdown']);
} elseif (isset($input['html'])) {
$newEditor = 'wysiwyg';
$pageContent->setNewHTML($input['html']);
}
if ($newEditor !== $currentEditor && userCan('editor-change')) {
$page->editor = $newEditor;
}
}
/**
@@ -229,8 +244,12 @@ class PageRepo
*/
protected function savePageRevision(Page $page, string $summary = null): PageRevision
{
$revision = new PageRevision($page->getAttributes());
$revision = new PageRevision();
$revision->name = $page->name;
$revision->html = $page->html;
$revision->markdown = $page->markdown;
$revision->text = $page->text;
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -260,10 +279,15 @@ class PageRepo
return $page;
}
// Otherwise save the data to a revision
// Otherwise, save the data to a revision
$draft = $this->getPageRevisionToUpdate($page);
$draft->fill($input);
if (setting('app-editor') !== 'markdown') {
if (!empty($input['markdown'])) {
$draft->markdown = $input['markdown'];
$draft->html = '';
} else {
$draft->html = $input['html'];
$draft->markdown = '';
}

View File

@@ -215,14 +215,16 @@ class ExportFormatter
*/
protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace embed tags with images
$htmlContent = preg_replace("/<embed (.*?)>/i", '<img $1>', $htmlContent);
// Replace image src with base64 encoded image strings
// Replace image & embed src attributes with base64 encoded data strings
$imageTagsOutput = [];
preg_match_all("/<img .*?src=['\"](.*?)['\"].*?>/i", $htmlContent, $imageTagsOutput);
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$srcString = $imageTagsOutput[1][$index];
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
@@ -232,14 +234,13 @@ class ExportFormatter
}
}
// Replace any relative links with full system URL
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
preg_match_all("/<a .*href=['\"](.*?)['\"].*?>/i", $htmlContent, $linksOutput);
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
$srcString = $linksOutput[1][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
@@ -248,7 +249,6 @@ class ExportFormatter
}
}
// Replace any relative links with system domain
return $htmlContent;
}
@@ -326,7 +326,7 @@ class ExportFormatter
$text .= $this->pageToMarkdown($page) . "\n\n";
}
return $text;
return trim($text);
}
/**
@@ -338,12 +338,12 @@ class ExportFormatter
$text = '# ' . $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild instanceof Chapter) {
$text .= $this->chapterToMarkdown($bookChild);
$text .= $this->chapterToMarkdown($bookChild) . "\n\n";
} else {
$text .= $this->pageToMarkdown($bookChild);
$text .= $this->pageToMarkdown($bookChild) . "\n\n";
}
}
return $text;
return trim($text);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\ConverterInterface;
use League\HTMLToMarkdown\ElementInterface;
class CheckboxConverter implements ConverterInterface
{
public function convert(ElementInterface $element): string
{
if (strtolower($element->getAttribute('type')) === 'checkbox') {
$isChecked = $element->getAttribute('checked') === 'checked';
return $isChecked ? ' [x] ' : ' [ ] ';
}
return $element->getValue();
}
/**
* @return string[]
*/
public function getSupportedTags(): array
{
return ['input'];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\DivConverter;
use League\HTMLToMarkdown\ElementInterface;
class CustomDivConverter extends DivConverter
{
public function convert(ElementInterface $element): string
{
// Clean up draw.io diagrams
$drawIoDiagram = $element->getAttribute('drawio-diagram');
if ($drawIoDiagram) {
return "<div drawio-diagram=\"{$drawIoDiagram}\">{$element->getValue()}</div>\n\n";
}
return parent::convert($element);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\ImageConverter;
use League\HTMLToMarkdown\ElementInterface;
class CustomImageConverter extends ImageConverter
{
public function convert(ElementInterface $element): string
{
$parent = $element->getParent();
// Remain as HTML if within diagram block.
$withinDrawing = $parent && !empty($parent->getAttribute('drawio-diagram'));
if ($withinDrawing) {
$src = e($element->getAttribute('src'));
$alt = e($element->getAttribute('alt'));
return "<img src=\"{$src}\" alt=\"{$alt}\"/>";
}
return parent::convert($element);
}
}

View File

@@ -9,7 +9,7 @@ class CustomParagraphConverter extends ParagraphConverter
{
public function convert(ElementInterface $element): string
{
$class = $element->getAttribute('class');
$class = e($element->getAttribute('class'));
if (strpos($class, 'callout') !== false) {
return "<{$element->getTagName()} class=\"{$class}\">{$element->getValue()}</{$element->getTagName()}>\n\n";
}

View File

@@ -5,12 +5,10 @@ namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\BlockquoteConverter;
use League\HTMLToMarkdown\Converter\CodeConverter;
use League\HTMLToMarkdown\Converter\CommentConverter;
use League\HTMLToMarkdown\Converter\DivConverter;
use League\HTMLToMarkdown\Converter\EmphasisConverter;
use League\HTMLToMarkdown\Converter\HardBreakConverter;
use League\HTMLToMarkdown\Converter\HeaderConverter;
use League\HTMLToMarkdown\Converter\HorizontalRuleConverter;
use League\HTMLToMarkdown\Converter\ImageConverter;
use League\HTMLToMarkdown\Converter\LinkConverter;
use League\HTMLToMarkdown\Converter\ListBlockConverter;
use League\HTMLToMarkdown\Converter\ListItemConverter;
@@ -21,7 +19,7 @@ use League\HTMLToMarkdown\HtmlConverter;
class HtmlToMarkdown
{
protected $html;
protected string $html;
public function __construct(string $html)
{
@@ -75,18 +73,20 @@ class HtmlToMarkdown
$environment->addConverter(new BlockquoteConverter());
$environment->addConverter(new CodeConverter());
$environment->addConverter(new CommentConverter());
$environment->addConverter(new DivConverter());
$environment->addConverter(new CustomDivConverter());
$environment->addConverter(new EmphasisConverter());
$environment->addConverter(new HardBreakConverter());
$environment->addConverter(new HeaderConverter());
$environment->addConverter(new HorizontalRuleConverter());
$environment->addConverter(new ImageConverter());
$environment->addConverter(new CustomImageConverter());
$environment->addConverter(new LinkConverter());
$environment->addConverter(new ListBlockConverter());
$environment->addConverter(new ListItemConverter());
$environment->addConverter(new CustomParagraphConverter());
$environment->addConverter(new PreformattedConverter());
$environment->addConverter(new TextConverter());
$environment->addConverter(new CheckboxConverter());
$environment->addConverter(new SpacedTagFallbackConverter());
return $environment;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class MarkdownToHtml
{
protected string $markdown;
public function __construct(string $markdown)
{
$this->markdown = $markdown;
}
public function convert(): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($this->markdown);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\ConverterInterface;
use League\HTMLToMarkdown\ElementInterface;
/**
* For certain defined tags, add additional spacing upon the retained HTML content
* to separate it out from anything that may be markdown soon afterwards or within.
*/
class SpacedTagFallbackConverter implements ConverterInterface
{
public function convert(ElementInterface $element): string
{
return \html_entity_decode($element->getChildrenAsString()) . "\n\n";
}
public function getSupportedTags(): array
{
return ['summary', 'iframe'];
}
}

View File

@@ -3,11 +3,8 @@
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomListItemRenderer;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use BookStack\Util\HtmlContentFilter;
@@ -17,15 +14,10 @@ use DOMNode;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class PageContent
{
protected $page;
protected Page $page;
/**
* PageContent constructor.
@@ -53,28 +45,11 @@ class PageContent
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
$html = $this->markdownToHtml($markdown);
$html = (new MarkdownToHtml($markdown))->convert();
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
}
/**
* Convert the given Markdown content to a HTML string.
*/
protected function markdownToHtml(string $markdown): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($markdown);
}
/**
* Convert all base64 image data to saved images.
*/

View File

@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
class PageEditActivity
{
protected $page;
protected Page $page;
/**
* PageEditActivity constructor.

View File

@@ -0,0 +1,115 @@
<?php
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
class PageEditorData
{
protected Page $page;
protected PageRepo $pageRepo;
protected string $requestedEditor;
protected array $viewData;
protected array $warnings;
public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
{
$this->page = $page;
$this->pageRepo = $pageRepo;
$this->requestedEditor = $requestedEditor;
$this->viewData = $this->build();
}
public function getViewData(): array
{
return $this->viewData;
}
public function getWarnings(): array
{
return $this->warnings;
}
protected function build(): array
{
$page = clone $this->page;
$isDraft = boolval($this->page->draft);
$templates = $this->pageRepo->getTemplates(10);
$draftsEnabled = auth()->check();
$isDraftRevision = false;
$this->warnings = [];
$editActivity = new PageEditActivity($page);
if ($editActivity->hasActiveEditing()) {
$this->warnings[] = $editActivity->activeEditingMessage();
}
// Check for a current draft version for this user
$userDraft = $this->pageRepo->getUserDraft($page);
if ($userDraft !== null) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$isDraftRevision = true;
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
$editorType = $this->getEditorType($page);
$this->updateContentForEditor($page, $editorType);
return [
'page' => $page,
'book' => $page->book,
'isDraft' => $isDraft,
'isDraftRevision' => $isDraftRevision,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
'editor' => $editorType,
];
}
protected function updateContentForEditor(Page $page, string $editorType): void
{
$isHtml = !empty($page->html) && empty($page->markdown);
// HTML to markdown-clean conversion
if ($editorType === 'markdown' && $isHtml && $this->requestedEditor === 'markdown-clean') {
$page->markdown = (new HtmlToMarkdown($page->html))->convert();
}
// Markdown to HTML conversion if we don't have HTML
if ($editorType === 'wysiwyg' && !$isHtml) {
$page->html = (new MarkdownToHtml($page->markdown))->convert();
}
}
/**
* Get the type of editor to show for editing the given page.
* Defaults based upon the current content of the page otherwise will fall back
* to system default but will take a requested type (if provided) if permissions allow.
*/
protected function getEditorType(Page $page): string
{
$editorType = $page->editor ?: self::getSystemDefaultEditor();
// Use requested editor if valid and if we have permission
$requestedType = explode('-', $this->requestedEditor)[0];
if (($requestedType === 'markdown' || $requestedType === 'wysiwyg') && userCan('editor-change')) {
$editorType = $requestedType;
}
return $editorType;
}
/**
* Get the configured system default editor.
*/
public static function getSystemDefaultEditor(): string
{
return setting('app-editor') === 'markdown' ? 'markdown' : 'wysiwyg';
}
}

View File

@@ -360,7 +360,7 @@ class SearchRunner
/** @var Connection $connection */
$connection = $query->getConnection();
$tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
$query->whereRaw("value ${tagOperator} ${tagValue}");
$query->whereRaw("value {$tagOperator} {$tagValue}");
} else {
$query->where('value', $tagOperator, $tagValue);
}

View File

@@ -19,10 +19,13 @@ class JsonDebugException extends Exception
}
/**
* Covert this exception into a response.
* Convert this exception into a response.
* We add a manual data conversion to UTF8 to ensure any binary data is presentable as a JSON string.
*/
public function render(): JsonResponse
{
return response()->json($this->data);
$cleaned = mb_convert_encoding($this->data, 'UTF-8');
return response()->json($cleaned);
}
}

View File

@@ -87,14 +87,33 @@ class AttachmentApiController extends ApiController
'markdown' => $attachment->markdownLink(),
]);
if (!$attachment->external) {
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
$attachment->setAttribute('content', base64_encode($attachmentContents));
} else {
// Simply return a JSON response of the attachment for link-based attachments
if ($attachment->external) {
$attachment->setAttribute('content', $attachment->path);
return response()->json($attachment);
}
return response()->json($attachment);
// Build and split our core JSON, at point of content.
$splitter = 'CONTENT_SPLIT_LOCATION_' . time() . '_' . rand(1, 40000);
$attachment->setAttribute('content', $splitter);
$json = $attachment->toJson();
$jsonParts = explode($splitter, $json);
// Get a stream for the file data from storage
$stream = $this->attachmentService->streamAttachmentFromStorage($attachment);
return response()->stream(function () use ($jsonParts, $stream) {
// Output the pre-content JSON data
echo $jsonParts[0];
// Stream out our attachment data as base64 content
stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ);
fpassthru($stream);
fclose($stream);
// Output our post-content JSON data
echo $jsonParts[1];
}, 200, ['Content-Type' => 'application/json']);
}
/**

View File

@@ -11,21 +11,20 @@ use Illuminate\Validation\ValidationException;
class BookshelfApiController extends ApiController
{
/**
* @var BookshelfRepo
*/
protected $bookshelfRepo;
protected BookshelfRepo $bookshelfRepo;
protected $rules = [
'create' => [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'books' => ['array'],
'tags' => ['array'],
],
'update' => [
'name' => ['string', 'min:1', 'max:255'],
'description' => ['string', 'max:1000'],
'books' => ['array'],
'tags' => ['array'],
],
];

View File

@@ -12,7 +12,7 @@ use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected $pageRepo;
protected PageRepo $pageRepo;
protected $rules = [
'create' => [
@@ -24,8 +24,8 @@ class PageApiController extends ApiController
'tags' => ['array'],
],
'update' => [
'book_id' => ['required', 'integer'],
'chapter_id' => ['required', 'integer'],
'book_id' => ['integer'],
'chapter_id' => ['integer'],
'name' => ['string', 'min:1', 'max:255'],
'html' => ['string'],
'markdown' => ['string'],
@@ -103,6 +103,8 @@ class PageApiController extends ApiController
*/
public function update(Request $request, string $id)
{
$requestData = $this->validate($request, $this->rules['update']);
$page = $this->pageRepo->getById($id, []);
$this->checkOwnablePermission('page-update', $page);
@@ -127,7 +129,7 @@ class PageApiController extends ApiController
}
}
$updatedPage = $this->pageRepo->update($page, $request->all());
$updatedPage = $this->pageRepo->update($page, $requestData);
return response()->json($updatedPage->forJsonDisplay());
}

View File

@@ -0,0 +1,90 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Repos\DeletionRepo;
use Closure;
use Illuminate\Database\Eloquent\Builder;
class RecycleBinApiController extends ApiController
{
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
return $next($request);
});
}
/**
* Get a top-level listing of the items in the recycle bin.
* The "deletable" property will reflect the main item deleted.
* For books and chapters, counts of child pages/chapters will
* be loaded within this "deletable" data.
* For chapters & pages, the parent item will be loaded within this "deletable" data.
* Requires permission to manage both system settings and permissions.
*/
public function list()
{
return $this->apiListingResponse(Deletion::query()->with('deletable'), [
'id',
'deleted_by',
'created_at',
'updated_at',
'deletable_type',
'deletable_id',
], [Closure::fromCallable([$this, 'listFormatter'])]);
}
/**
* Restore a single deletion from the recycle bin.
* Requires permission to manage both system settings and permissions.
*/
public function restore(DeletionRepo $deletionRepo, string $deletionId)
{
$restoreCount = $deletionRepo->restore(intval($deletionId));
return response()->json(['restore_count' => $restoreCount]);
}
/**
* Remove a single deletion from the recycle bin.
* Use this endpoint carefully as it will entirely remove the underlying deleted items from the system.
* Requires permission to manage both system settings and permissions.
*/
public function destroy(DeletionRepo $deletionRepo, string $deletionId)
{
$deleteCount = $deletionRepo->destroy(intval($deletionId));
return response()->json(['delete_count' => $deleteCount]);
}
/**
* Load some related details for the deletion listing.
*/
protected function listFormatter(Deletion $deletion)
{
$deletable = $deletion->deletable;
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
if ($deletable instanceof BookChild) {
$parent = $deletable->getParent();
$parent->setAttribute('type', $parent->getType());
$deletable->setRelation('parent', $parent);
}
if ($deletable instanceof Book || $deletable instanceof Chapter) {
$countsToLoad = ['pages' => $withTrashedQuery];
if ($deletable instanceof Book) {
$countsToLoad['chapters'] = $withTrashedQuery;
}
$deletable->loadCount($countsToLoad);
}
}
}

View File

@@ -15,8 +15,8 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $pageRepo;
protected AttachmentService $attachmentService;
protected PageRepo $pageRepo;
/**
* AttachmentController constructor.
@@ -230,13 +230,13 @@ class AttachmentController extends Controller
}
$fileName = $attachment->getFileName();
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
return $this->streamedInlineDownloadResponse($attachmentStream, $fileName);
}
return $this->downloadResponse($attachmentContents, $fileName);
return $this->streamedDownloadResponse($attachmentStream, $fileName);
}
/**

View File

@@ -12,6 +12,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
use Symfony\Component\HttpFoundation\StreamedResponse;
abstract class Controller extends BaseController
{
@@ -115,7 +116,30 @@ abstract class Controller extends BaseController
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
/**
* Create a response that forces a download, from a given stream of content.
*/
protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse
{
return response()->stream(function () use ($stream) {
// End & flush the output buffer, if we're in one, otherwise we still use memory.
// Output buffer may or may not exist depending on PHP `output_buffering` setting.
// Ignore in testing since output buffers are used to gather a response.
if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
ob_end_clean();
}
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
@@ -130,7 +154,28 @@ abstract class Controller extends BaseController
return response()->make($content, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
/**
* Create a file download response that provides the file with a content-type
* correct for the file, in a way so the browser can show the content in browser,
* for a given content stream.
*/
protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse
{
$sniffContent = fread($stream, 1000);
$mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
return response()->stream(function () use ($sniffContent, $stream) {
echo $sniffContent;
fpassthru($stream);
fclose($stream);
}, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
@@ -174,6 +219,6 @@ abstract class Controller extends BaseController
*/
protected function getImageValidationRules(): array
{
return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
return ['image_extension', 'mimes:jpeg,png,gif,webp,svg', 'max:' . (config('app.upload_limit') * 1000)];
}
}

View File

@@ -76,8 +76,11 @@ class DrawioImageController extends Controller
return $this->jsonError('Image data could not be found');
}
$isSvg = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
$uriPrefix = $isSvg ? 'data:image/svg+xml;base64,' : 'data:image/png;base64,';
return response()->json([
'content' => base64_encode($imageData),
'content' => $uriPrefix . base64_encode($imageData),
]);
}
}

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\Cloner;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
@@ -21,7 +22,7 @@ use Throwable;
class PageController extends Controller
{
protected $pageRepo;
protected PageRepo $pageRepo;
/**
* PageController constructor.
@@ -82,22 +83,15 @@ class PageController extends Controller
*
* @throws NotFoundException
*/
public function editDraft(string $bookSlug, int $pageId)
public function editDraft(Request $request, string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
$editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->isSignedIn();
$templates = $this->pageRepo->getTemplates(10);
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
]);
return view('pages.edit', $editorData->getViewData());
}
/**
@@ -188,43 +182,19 @@ class PageController extends Controller
*
* @throws NotFoundException
*/
public function edit(string $bookSlug, string $pageSlug)
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$page->isDraft = false;
$editActivity = new PageEditActivity($page);
// Check for active editing
$warnings = [];
if ($editActivity->hasActiveEditing()) {
$warnings[] = $editActivity->activeEditingMessage();
$editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
if ($editorData->getWarnings()) {
$this->showWarningNotification(implode("\n", $editorData->getWarnings()));
}
// Check for a current draft version for this user
$userDraft = $this->pageRepo->getUserDraft($page);
if ($userDraft !== null) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$page->isDraft = true;
$warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
if (count($warnings) > 0) {
$this->showWarningNotification(implode("\n", $warnings));
}
$templates = $this->pageRepo->getTemplates(10);
$draftsEnabled = $this->isSignedIn();
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
]);
return view('pages.edit', $editorData->getViewData());
}
/**

View File

@@ -124,11 +124,8 @@ class PageRevisionController extends Controller
throw new NotFoundException("Revision #{$revId} not found");
}
// Get the current revision for the page
$currentRevision = $page->getCurrentRevision();
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
// Check if it's the latest revision, cannot delete the latest revision.
if (intval($page->currentRevision->id ?? null) === intval($revId)) {
$this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
return redirect($page->getUrl('/revisions'));

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Entities\Tools\TrashCan;
class RecycleBinController extends Controller
@@ -73,12 +74,9 @@ class RecycleBinController extends Controller
*
* @throws \Exception
*/
public function restore(string $id)
public function restore(DeletionRepo $deletionRepo, string $id)
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
$this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
$restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
$restoreCount = $deletionRepo->restore((int) $id);
$this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
@@ -103,12 +101,9 @@ class RecycleBinController extends Controller
*
* @throws \Exception
*/
public function destroy(string $id)
public function destroy(DeletionRepo $deletionRepo, string $id)
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
$this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
$deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
$deleteCount = $deletionRepo->destroy((int) $id);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));

View File

@@ -9,28 +9,37 @@ use Illuminate\Http\Request;
class SettingController extends Controller
{
protected $imageRepo;
protected ImageRepo $imageRepo;
protected array $settingCategories = ['features', 'customization', 'registration'];
/**
* SettingController constructor.
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
}
/**
* Display a listing of the settings.
* Handle requests to the settings index path.
*/
public function index()
{
return redirect('/settings/features');
}
/**
* Display the settings for the given category.
*/
public function category(string $category)
{
$this->ensureCategoryExists($category);
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings.index', [
return view('settings.' . $category, [
'category' => $category,
'version' => $version,
'guestUser' => User::getDefault(),
]);
@@ -39,8 +48,9 @@ class SettingController extends Controller
/**
* Update the specified settings in storage.
*/
public function update(Request $request)
public function update(Request $request, string $category)
{
$this->ensureCategoryExists($category);
$this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
@@ -57,7 +67,7 @@ class SettingController extends Controller
}
// Update logo image if set
if ($request->hasFile('app_logo')) {
if ($category === 'customization' && $request->hasFile('app_logo')) {
$logoFile = $request->file('app_logo');
$this->imageRepo->destroyByType('system');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
@@ -65,16 +75,21 @@ class SettingController extends Controller
}
// Clear logo image if requested
if ($request->get('app_logo_reset', null)) {
if ($category === 'customization' && $request->get('app_logo_reset', null)) {
$this->imageRepo->destroyByType('system');
setting()->remove('app-logo');
}
$section = $request->get('section', '');
$this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
$this->logActivity(ActivityType::SETTINGS_UPDATE, $category);
$this->showSuccessNotification(trans('settings.settings_save_success'));
$redirectLocation = '/settings#' . $section;
return redirect(rtrim($redirectLocation, '#'));
return redirect("/settings/{$category}");
}
protected function ensureCategoryExists(string $category): void
{
if (!in_array($category, $this->settingCategories)) {
abort(404);
}
}
}

View File

@@ -11,7 +11,7 @@ class Localization
/**
* Array of right-to-left locales.
*/
protected $rtlLocales = ['ar', 'he'];
protected $rtlLocales = ['ar', 'fa', 'he'];
/**
* Map of BookStack locale names to best-estimate system locale names.
@@ -29,6 +29,8 @@ class Localization
'es' => 'es_ES',
'es_AR' => 'es_AR',
'et' => 'et_EE',
'eu' => 'eu_ES',
'fa' => 'fa_IR',
'fr' => 'fr_FR',
'he' => 'he_IL',
'hr' => 'hr_HR',

View File

@@ -8,20 +8,38 @@ class Request extends LaravelRequest
{
/**
* Override the default request methods to get the scheme and host
* to set the custom APP_URL, if set.
* to directly use the custom APP_URL, if set.
*
* @return \Illuminate\Config\Repository|mixed|string
* @return string
*/
public function getSchemeAndHttpHost()
{
$base = config('app.url', null);
$appUrl = config('app.url', null);
if ($base) {
$base = trim($base, '/');
} else {
$base = $this->getScheme() . '://' . $this->getHttpHost();
if ($appUrl) {
return implode('/', array_slice(explode('/', $appUrl), 0, 3));
}
return $base;
return parent::getSchemeAndHttpHost();
}
/**
* Override the default request methods to get the base URL
* to directly use the custom APP_URL, if set.
* The base URL never ends with a / but should start with one if not empty.
*
* @return string
*/
public function getBaseUrl()
{
$appUrl = config('app.url', null);
if ($appUrl) {
$parsedBaseUrl = rtrim(implode('/', array_slice(explode('/', $appUrl), 3)), '/');
return empty($parsedBaseUrl) ? '' : ('/' . $parsedBaseUrl);
}
return parent::getBaseUrl();
}
}

View File

@@ -51,12 +51,12 @@ class AppServiceProvider extends ServiceProvider
// Allow longer string lengths after upgrade to utf8mb4
Schema::defaultStringLength(191);
// Set morph-map due to namespace changes
Relation::morphMap([
'BookStack\\Bookshelf' => Bookshelf::class,
'BookStack\\Book' => Book::class,
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
// Set morph-map for our relations to friendlier aliases
Relation::enforceMorphMap([
'bookshelf' => Bookshelf::class,
'book' => Book::class,
'chapter' => Chapter::class,
'page' => Page::class,
]);
// View Composers

View File

@@ -93,6 +93,8 @@ class ThemeEvents
* @param string $event
* @param \BookStack\Actions\Webhook $webhook
* @param string|\BookStack\Interfaces\Loggable $detail
* @param \BookStack\Auth\User $initiator
* @param int $initiatedTime
*/
const WEBHOOK_CALL_BEFORE = 'webhook_call_before';
}

View File

@@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
{
protected $fileSystem;
protected FilesystemManager $fileSystem;
/**
* AttachmentService constructor.
@@ -73,6 +73,18 @@ class AttachmentService
return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
}
/**
* Stream an attachment from storage.
*
* @throws FileNotFoundException
*
* @return resource|null
*/
public function streamAttachmentFromStorage(Attachment $attachment)
{
return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path));
}
/**
* Store a new attachment upon user upload.
*
@@ -211,8 +223,6 @@ class AttachmentService
*/
protected function putFileInStorage(UploadedFile $uploadedFile): string
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorageDisk();
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
@@ -221,10 +231,11 @@ class AttachmentService
$uploadFileName = Str::random(3) . $uploadFileName;
}
$attachmentStream = fopen($uploadedFile->getRealPath(), 'r');
$attachmentPath = $basePath . $uploadFileName;
try {
$storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData);
$storage->writeStream($this->adjustPathForStorageDisk($attachmentPath), $attachmentStream);
} catch (Exception $e) {
Log::error('Error when attempting file upload:' . $e->getMessage());

View File

@@ -148,7 +148,8 @@ class ImageRepo
*/
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
{
$name = 'Drawing-' . user()->id . '-' . time() . '.png';
$isSvg = strpos($base64Uri, 'data:image/svg+xml;') === 0;
$name = 'Drawing-' . user()->id . '-' . time() . ($isSvg ? '.svg' : '.png');
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Uploads;
use BookStack\Exceptions\ImageUploadException;
use ErrorException;
use Exception;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
@@ -14,6 +15,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\Image as InterventionImage;
use Intervention\Image\ImageManager;
use League\Flysystem\Util;
use Psr\SimpleCache\InvalidArgumentException;
@@ -28,7 +30,7 @@ class ImageService
protected $image;
protected $fileSystem;
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
/**
* ImageService constructor.
@@ -228,6 +230,14 @@ class ImageService
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}
/**
* Check if the given image is an SVG image file.
*/
protected function isSvg(Image $image): bool
{
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
}
/**
* Check if the given image and image data is apng.
*/
@@ -253,8 +263,8 @@ class ImageService
*/
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
{
// Do not resize GIF images where we're not cropping
if ($keepRatio && $this->isGif($image)) {
// Do not resize GIF images where we're not cropping or SVG images.
if (($keepRatio && $this->isGif($image)) || $this->isSvg($image)) {
return $this->getPublicUrl($image->path);
}
@@ -308,6 +318,8 @@ class ImageService
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
}
$this->orientImageToOriginalExif($thumb, $imageData);
if ($keepRatio) {
$thumb->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
@@ -328,6 +340,49 @@ class ImageService
return $thumbData;
}
/**
* Orientate the given intervention image based upon the given original image data.
* Intervention does have an `orientate` method but the exif data it needs is lost before it
* can be used (At least when created using binary string data) so we need to do some
* implementation on our side to use the original image data.
* Bulk of logic taken from: https://github.com/Intervention/image/blob/b734a4988b2148e7d10364b0609978a88d277536/src/Intervention/Image/Commands/OrientateCommand.php
* Copyright (c) Oliver Vogel, MIT License.
*/
protected function orientImageToOriginalExif(InterventionImage $image, string $originalData): void
{
if (!extension_loaded('exif')) {
return;
}
$stream = Utils::streamFor($originalData)->detach();
$exif = @exif_read_data($stream);
$orientation = $exif ? ($exif['Orientation'] ?? null) : null;
switch ($orientation) {
case 2:
$image->flip();
break;
case 3:
$image->rotate(180);
break;
case 4:
$image->rotate(180)->flip();
break;
case 5:
$image->rotate(270)->flip();
break;
case 6:
$image->rotate(270);
break;
case 7:
$image->rotate(90)->flip();
break;
case 8:
$image->rotate(90);
break;
}
}
/**
* Get the raw data content from an image.
*

View File

@@ -22,7 +22,7 @@ class CspService
}
/**
* Get the CSP headers for the application
* Get the CSP headers for the application.
*/
public function getCspHeader(): string
{
@@ -86,6 +86,7 @@ class CspService
{
$iframeHosts = $this->getAllowedIframeHosts();
array_unshift($iframeHosts, "'self'");
return 'frame-ancestors ' . implode(' ', $iframeHosts);
}
@@ -97,6 +98,7 @@ class CspService
{
$iframeHosts = $this->getAllowedIframeSources();
array_unshift($iframeHosts, "'self'");
return 'frame-src ' . implode(' ', $iframeHosts);
}

847
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddEditorChangeFieldAndPermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Add the new 'editor' column to the pages table
Schema::table('pages', function (Blueprint $table) {
$table->string('editor', 50)->default('');
});
// Populate the new 'editor' column
// We set it to 'markdown' for pages currently with markdown content
DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);
// We set it to 'wysiwyg' where we have HTML but no markdown
DB::table('pages')->where('markdown', '=', '')
->where('html', '!=', '')
->update(['editor' => 'wysiwyg']);
// Give the admin user permission to change the editor
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => 'editor-change',
'display_name' => 'Change page editor',
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId,
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Drop the new column from the pages table
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('editor');
});
// Remove traces of the role permission
DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();
}
}

View File

@@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class UpdatePolymorphicTypes extends Migration
{
/**
* Mapping of old polymorphic types to new simpler values.
*/
protected $changeMap = [
'BookStack\\Bookshelf' => 'bookshelf',
'BookStack\\Book' => 'book',
'BookStack\\Chapter' => 'chapter',
'BookStack\\Page' => 'page',
];
/**
* Mapping of tables and columns that contain polymorphic types.
*/
protected $columnsByTable = [
'activities' => 'entity_type',
'comments' => 'entity_type',
'deletions' => 'deletable_type',
'entity_permissions' => 'restrictable_type',
'favourites' => 'favouritable_type',
'joint_permissions' => 'entity_type',
'search_terms' => 'entity_type',
'tags' => 'entity_type',
'views' => 'viewable_type',
];
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
foreach ($this->columnsByTable as $table => $column) {
foreach ($this->changeMap as $oldVal => $newVal) {
DB::table($table)
->where([$column => $oldVal])
->update([$column => $newVal]);
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
foreach ($this->columnsByTable as $table => $column) {
foreach ($this->changeMap as $oldVal => $newVal) {
DB::table($table)
->where([$column => $newVal])
->update([$column => $oldVal]);
}
}
}
}

View File

@@ -0,0 +1,3 @@
{
"delete_count": 2
}

View File

@@ -0,0 +1,64 @@
{
"data": [
{
"id": 18,
"deleted_by": 1,
"created_at": "2022-04-20T12:57:46.000000Z",
"updated_at": "2022-04-20T12:57:46.000000Z",
"deletable_type": "page",
"deletable_id": 2582,
"deletable": {
"id": 2582,
"book_id": 25,
"chapter_id": 0,
"name": "A Wonderful Page",
"slug": "a-wonderful-page",
"priority": 9,
"created_at": "2022-02-08T00:44:45.000000Z",
"updated_at": "2022-04-20T12:57:46.000000Z",
"created_by": 1,
"updated_by": 1,
"draft": false,
"revision_count": 1,
"template": false,
"owned_by": 1,
"editor": "wysiwyg",
"book_slug": "a-great-book",
"parent": {
"id": 25,
"name": "A Great Book",
"slug": "a-great-book",
"description": "",
"created_at": "2022-01-24T16:14:28.000000Z",
"updated_at": "2022-03-06T15:14:50.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
"type": "book"
}
}
},
{
"id": 19,
"deleted_by": 1,
"created_at": "2022-04-25T16:07:46.000000Z",
"updated_at": "2022-04-25T16:07:46.000000Z",
"deletable_type": "book",
"deletable_id": 13,
"deletable": {
"id": 13,
"name": "A Big Book!",
"slug": "a-big-book",
"description": "This is a very large book with loads of cool stuff in it!",
"created_at": "2021-11-08T11:26:43.000000Z",
"updated_at": "2022-04-25T16:07:47.000000Z",
"created_by": 27,
"updated_by": 1,
"owned_by": 1,
"pages_count": 208,
"chapters_count": 50
}
}
],
"total": 2
}

View File

@@ -0,0 +1,3 @@
{
"restore_count": 2
}

32
dev/build/esbuild.js Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env node
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');
// Check if we're building for production
// (Set via passing `production` as first argument)
const isProd = process.argv[2] === 'production';
// Gather our input files
const jsInDir = path.join(__dirname, '../../resources/js');
const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8');
const entryFiles = jsInDirFiles
.filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
.map(f => path.join(jsInDir, f));
// Locate our output directory
const outDir = path.join(__dirname, '../../public/dist');
// Build via esbuild
esbuild.build({
bundle: true,
entryPoints: entryFiles,
outdir: outDir,
sourcemap: true,
target: 'es2020',
mainFields: ['module', 'main'],
format: 'esm',
minify: isProd,
logLevel: "info",
}).catch(() => process.exit(1));

384
package-lock.json generated
View File

@@ -4,28 +4,27 @@
"requires": true,
"packages": {
"": {
"name": "bookstack",
"dependencies": {
"clipboard": "^2.0.10",
"codemirror": "^5.65.2",
"dropzone": "^5.9.3",
"markdown-it": "^12.3.2",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.14.0"
"sortablejs": "^1.15.0"
},
"devDependencies": {
"chokidar-cli": "^3.0",
"esbuild": "0.14.23",
"esbuild": "0.14.36",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.49.8"
"sass": "^1.50.0"
}
},
"node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true,
"engines": {
"node": ">=6"
@@ -342,9 +341,9 @@
}
},
"node_modules/esbuild": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.23.tgz",
"integrity": "sha512-XjnIcZ9KB6lfonCa+jRguXyRYcldmkyZ99ieDksqW/C8bnyEX299yA4QH2XcgijCgaddEZePPTgvx/2imsq7Ig==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz",
"integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -354,31 +353,48 @@
"node": ">=12"
},
"optionalDependencies": {
"esbuild-android-arm64": "0.14.23",
"esbuild-darwin-64": "0.14.23",
"esbuild-darwin-arm64": "0.14.23",
"esbuild-freebsd-64": "0.14.23",
"esbuild-freebsd-arm64": "0.14.23",
"esbuild-linux-32": "0.14.23",
"esbuild-linux-64": "0.14.23",
"esbuild-linux-arm": "0.14.23",
"esbuild-linux-arm64": "0.14.23",
"esbuild-linux-mips64le": "0.14.23",
"esbuild-linux-ppc64le": "0.14.23",
"esbuild-linux-riscv64": "0.14.23",
"esbuild-linux-s390x": "0.14.23",
"esbuild-netbsd-64": "0.14.23",
"esbuild-openbsd-64": "0.14.23",
"esbuild-sunos-64": "0.14.23",
"esbuild-windows-32": "0.14.23",
"esbuild-windows-64": "0.14.23",
"esbuild-windows-arm64": "0.14.23"
"esbuild-android-64": "0.14.36",
"esbuild-android-arm64": "0.14.36",
"esbuild-darwin-64": "0.14.36",
"esbuild-darwin-arm64": "0.14.36",
"esbuild-freebsd-64": "0.14.36",
"esbuild-freebsd-arm64": "0.14.36",
"esbuild-linux-32": "0.14.36",
"esbuild-linux-64": "0.14.36",
"esbuild-linux-arm": "0.14.36",
"esbuild-linux-arm64": "0.14.36",
"esbuild-linux-mips64le": "0.14.36",
"esbuild-linux-ppc64le": "0.14.36",
"esbuild-linux-riscv64": "0.14.36",
"esbuild-linux-s390x": "0.14.36",
"esbuild-netbsd-64": "0.14.36",
"esbuild-openbsd-64": "0.14.36",
"esbuild-sunos-64": "0.14.36",
"esbuild-windows-32": "0.14.36",
"esbuild-windows-64": "0.14.36",
"esbuild-windows-arm64": "0.14.36"
}
},
"node_modules/esbuild-android-64": {
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz",
"integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz",
"integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz",
"integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==",
"cpu": [
"arm64"
],
@@ -392,9 +408,9 @@
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz",
"integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz",
"integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==",
"cpu": [
"x64"
],
@@ -408,9 +424,9 @@
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz",
"integrity": "sha512-yat73Z/uJ5tRcfRiI4CCTv0FSnwErm3BJQeZAh+1tIP0TUNh6o+mXg338Zl5EKChD+YGp6PN+Dbhs7qa34RxSw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz",
"integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==",
"cpu": [
"arm64"
],
@@ -424,9 +440,9 @@
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz",
"integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz",
"integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==",
"cpu": [
"x64"
],
@@ -440,9 +456,9 @@
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz",
"integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz",
"integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==",
"cpu": [
"arm64"
],
@@ -456,9 +472,9 @@
}
},
"node_modules/esbuild-linux-32": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz",
"integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz",
"integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==",
"cpu": [
"ia32"
],
@@ -472,9 +488,9 @@
}
},
"node_modules/esbuild-linux-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz",
"integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz",
"integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==",
"cpu": [
"x64"
],
@@ -488,9 +504,9 @@
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz",
"integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz",
"integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==",
"cpu": [
"arm"
],
@@ -504,9 +520,9 @@
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz",
"integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz",
"integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==",
"cpu": [
"arm64"
],
@@ -520,9 +536,9 @@
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz",
"integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz",
"integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==",
"cpu": [
"mips64el"
],
@@ -536,9 +552,9 @@
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz",
"integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz",
"integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==",
"cpu": [
"ppc64"
],
@@ -552,9 +568,9 @@
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz",
"integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz",
"integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==",
"cpu": [
"riscv64"
],
@@ -568,9 +584,9 @@
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz",
"integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz",
"integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==",
"cpu": [
"s390x"
],
@@ -584,9 +600,9 @@
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz",
"integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz",
"integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==",
"cpu": [
"x64"
],
@@ -600,9 +616,9 @@
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz",
"integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz",
"integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==",
"cpu": [
"x64"
],
@@ -616,9 +632,9 @@
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz",
"integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz",
"integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==",
"cpu": [
"x64"
],
@@ -632,9 +648,9 @@
}
},
"node_modules/esbuild-windows-32": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz",
"integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz",
"integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==",
"cpu": [
"ia32"
],
@@ -648,9 +664,9 @@
}
},
"node_modules/esbuild-windows-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz",
"integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz",
"integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==",
"cpu": [
"x64"
],
@@ -664,9 +680,9 @@
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz",
"integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz",
"integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==",
"cpu": [
"arm64"
],
@@ -1504,9 +1520,9 @@
}
},
"node_modules/sass": {
"version": "1.49.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
"integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
"version": "1.50.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz",
"integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -1582,9 +1598,9 @@
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
},
"node_modules/source-map-js": {
"version": "1.0.2",
@@ -1870,9 +1886,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
},
"ansi-styles": {
@@ -2130,162 +2146,170 @@
}
},
"esbuild": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.23.tgz",
"integrity": "sha512-XjnIcZ9KB6lfonCa+jRguXyRYcldmkyZ99ieDksqW/C8bnyEX299yA4QH2XcgijCgaddEZePPTgvx/2imsq7Ig==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz",
"integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==",
"dev": true,
"requires": {
"esbuild-android-arm64": "0.14.23",
"esbuild-darwin-64": "0.14.23",
"esbuild-darwin-arm64": "0.14.23",
"esbuild-freebsd-64": "0.14.23",
"esbuild-freebsd-arm64": "0.14.23",
"esbuild-linux-32": "0.14.23",
"esbuild-linux-64": "0.14.23",
"esbuild-linux-arm": "0.14.23",
"esbuild-linux-arm64": "0.14.23",
"esbuild-linux-mips64le": "0.14.23",
"esbuild-linux-ppc64le": "0.14.23",
"esbuild-linux-riscv64": "0.14.23",
"esbuild-linux-s390x": "0.14.23",
"esbuild-netbsd-64": "0.14.23",
"esbuild-openbsd-64": "0.14.23",
"esbuild-sunos-64": "0.14.23",
"esbuild-windows-32": "0.14.23",
"esbuild-windows-64": "0.14.23",
"esbuild-windows-arm64": "0.14.23"
"esbuild-android-64": "0.14.36",
"esbuild-android-arm64": "0.14.36",
"esbuild-darwin-64": "0.14.36",
"esbuild-darwin-arm64": "0.14.36",
"esbuild-freebsd-64": "0.14.36",
"esbuild-freebsd-arm64": "0.14.36",
"esbuild-linux-32": "0.14.36",
"esbuild-linux-64": "0.14.36",
"esbuild-linux-arm": "0.14.36",
"esbuild-linux-arm64": "0.14.36",
"esbuild-linux-mips64le": "0.14.36",
"esbuild-linux-ppc64le": "0.14.36",
"esbuild-linux-riscv64": "0.14.36",
"esbuild-linux-s390x": "0.14.36",
"esbuild-netbsd-64": "0.14.36",
"esbuild-openbsd-64": "0.14.36",
"esbuild-sunos-64": "0.14.36",
"esbuild-windows-32": "0.14.36",
"esbuild-windows-64": "0.14.36",
"esbuild-windows-arm64": "0.14.36"
}
},
"esbuild-android-64": {
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz",
"integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==",
"dev": true,
"optional": true
},
"esbuild-android-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz",
"integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz",
"integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz",
"integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz",
"integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz",
"integrity": "sha512-yat73Z/uJ5tRcfRiI4CCTv0FSnwErm3BJQeZAh+1tIP0TUNh6o+mXg338Zl5EKChD+YGp6PN+Dbhs7qa34RxSw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz",
"integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz",
"integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz",
"integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz",
"integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz",
"integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz",
"integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz",
"integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz",
"integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz",
"integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz",
"integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz",
"integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz",
"integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz",
"integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz",
"integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz",
"integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz",
"integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz",
"integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==",
"dev": true,
"optional": true
},
"esbuild-linux-riscv64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz",
"integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz",
"integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==",
"dev": true,
"optional": true
},
"esbuild-linux-s390x": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz",
"integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz",
"integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz",
"integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz",
"integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz",
"integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz",
"integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==",
"dev": true,
"optional": true
},
"esbuild-sunos-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz",
"integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz",
"integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz",
"integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz",
"integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz",
"integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz",
"integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.14.23",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz",
"integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==",
"version": "0.14.36",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz",
"integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==",
"dev": true,
"optional": true
},
@@ -2886,9 +2910,9 @@
}
},
"sass": {
"version": "1.49.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
"integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
"version": "1.50.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz",
"integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -2946,9 +2970,9 @@
}
},
"sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
},
"source-map-js": {
"version": "1.0.2",

View File

@@ -4,9 +4,9 @@
"build:css:dev": "sass ./resources/sass:./public/dist",
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
"build:js:dev": "esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --format=esm",
"build:js:dev": "node dev/build/esbuild.js",
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --minify --format=esm",
"build:js:production": "node dev/build/esbuild.js production",
"build": "npm-run-all --parallel build:*:dev",
"production": "npm-run-all --parallel build:*:production",
"dev": "npm-run-all --parallel watch livereload",
@@ -16,11 +16,11 @@
},
"devDependencies": {
"chokidar-cli": "^3.0",
"esbuild": "0.14.23",
"esbuild": "0.14.36",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.49.8"
"sass": "^1.50.0"
},
"dependencies": {
"clipboard": "^2.0.10",
@@ -28,6 +28,6 @@
"dropzone": "^5.9.3",
"markdown-it": "^12.3.2",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.14.0"
"sortablejs": "^1.15.0"
}
}

View File

@@ -9,7 +9,7 @@ parameters:
# The level 8 is the highest level
level: 1
phpVersion: 70300
phpVersion: 70400
bootstrapFiles:
- bootstrap/phpstan.php

View File

@@ -34,6 +34,8 @@
<server name="AVATAR_URL" value=""/>
<server name="LDAP_START_TLS" value="false"/>
<server name="LDAP_VERSION" value="3"/>
<server name="LDAP_DUMP_USER_DETAILS" value="false"/>
<server name="LDAP_DUMP_USER_GROUPS" value="false"/>
<server name="SESSION_SECURE_COOKIE" value="null"/>
<server name="STORAGE_TYPE" value="local"/>
<server name="STORAGE_ATTACHMENT_TYPE" value="local"/>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,2C6.49,2,2,6.49,2,12s4.49,10,10,10c1.38,0,2.5-1.12,2.5-2.5c0-0.61-0.23-1.2-0.64-1.67c-0.08-0.1-0.13-0.21-0.13-0.33 c0-0.28,0.22-0.5,0.5-0.5H16c3.31,0,6-2.69,6-6C22,6.04,17.51,2,12,2z M17.5,13c-0.83,0-1.5-0.67-1.5-1.5c0-0.83,0.67-1.5,1.5-1.5 s1.5,0.67,1.5,1.5C19,12.33,18.33,13,17.5,13z M14.5,9C13.67,9,13,8.33,13,7.5C13,6.67,13.67,6,14.5,6S16,6.67,16,7.5 C16,8.33,15.33,9,14.5,9z M5,11.5C5,10.67,5.67,10,6.5,10S8,10.67,8,11.5C8,12.33,7.33,13,6.5,13S5,12.33,5,11.5z M11,7.5 C11,8.33,10.33,9,9.5,9S8,8.33,8,7.5C8,6.67,8.67,6,9.5,6S11,6.67,11,7.5z"/></svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 16H14v-2H6.99v-3L3 15l3.99 4ZM21 9l-3.99-4v3H10v2h7.01v3z"/></svg>

After

Width:  |  Height:  |  Size: 141 B

View File

@@ -131,7 +131,7 @@ class AutoSuggest {
return this.hideSuggestions();
}
this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
this.list.style.display = 'block';
for (const button of this.list.querySelectorAll('button')) {
button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));

View File

@@ -96,7 +96,7 @@ class CodeEditor {
this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
this.historyList.innerHTML = historyKeys.map(key => {
const localTime = (new Date(parseInt(key))).toLocaleTimeString();
return `<li><button type="button" data-time="${key}">${localTime}</button></li>`;
return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
}).join('');
}

View File

@@ -0,0 +1,52 @@
import {onSelect} from "../services/dom";
/**
* Custom equivalent of window.confirm() using our popup component.
* Is promise based so can be used like so:
* `const result = await dialog.show()`
* @extends {Component}
*/
class ConfirmDialog {
setup() {
this.container = this.$el;
this.confirmButton = this.$refs.confirm;
this.res = null;
onSelect(this.confirmButton, () => {
this.sendResult(true);
this.getPopup().hide();
});
}
show() {
this.getPopup().show(null, () => {
this.sendResult(false);
});
return new Promise((res, rej) => {
this.res = res;
});
}
/**
* @returns {Popup}
*/
getPopup() {
return this.container.components.popup;
}
/**
* @param {Boolean} result
*/
sendResult(result) {
if (this.res) {
this.res(result)
this.res = null;
}
}
}
export default ConfirmDialog;

View File

@@ -10,6 +10,7 @@ import chapterToggle from "./chapter-toggle.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import collapsible from "./collapsible.js"
import confirmDialog from "./confirm-dialog"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
@@ -26,7 +27,6 @@ import headerMobileToggle from "./header-mobile-toggle.js"
import homepageControl from "./homepage-control.js"
import imageManager from "./image-manager.js"
import imagePicker from "./image-picker.js"
import index from "./index.js"
import listSortControl from "./list-sort-control.js"
import markdownEditor from "./markdown-editor.js"
import newUserPassword from "./new-user-password.js"
@@ -66,6 +66,7 @@ const componentMapping = {
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"collapsible": collapsible,
"confirm-dialog": confirmDialog,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
@@ -82,7 +83,6 @@ const componentMapping = {
"homepage-control": homepageControl,
"image-manager": imageManager,
"image-picker": imagePicker,
"index": index,
"list-sort-control": listSortControl,
"markdown-editor": markdownEditor,
"new-user-password": newUserPassword,

View File

@@ -18,10 +18,8 @@ class MarkdownEditor {
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
this.display = this.elem.querySelector('.markdown-display');
this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea');
this.display = this.$refs.display;
this.input = this.$refs.input;
this.cm = null;
this.Code = null;
@@ -32,23 +30,13 @@ class MarkdownEditor {
});
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
const displayLoad = () => {
this.displayDoc = this.display.contentDocument;
this.init(cmLoadPromise);
};
if (this.display.contentDocument.readyState === 'complete') {
displayLoad();
} else {
this.display.addEventListener('load', displayLoad.bind(this));
}
window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
markdownIt: this.markdown,
displayEl: this.display,
codeMirrorInstance: this.cm,
});
this.init(cmLoadPromise);
}
init(cmLoadPromise) {
@@ -56,17 +44,17 @@ class MarkdownEditor {
let lastClick = 0;
// Prevent markdown display link click redirect
this.displayDoc.addEventListener('click', event => {
let isDblClick = Date.now() - lastClick < 300;
this.display.addEventListener('click', event => {
const isDblClick = Date.now() - lastClick < 300;
let link = event.target.closest('a');
const link = event.target.closest('a');
if (link !== null) {
event.preventDefault();
window.open(link.getAttribute('href'));
return;
}
let drawing = event.target.closest('[drawio-diagram]');
const drawing = event.target.closest('[drawio-diagram]');
if (drawing !== null && isDblClick) {
this.actionEditDrawing(drawing);
return;
@@ -77,10 +65,10 @@ class MarkdownEditor {
// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
const button = event.target.closest('button[data-action]');
if (button === null) return;
let action = button.getAttribute('data-action');
const action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
@@ -132,35 +120,11 @@ class MarkdownEditor {
window.$events.emit('editor-markdown-change', content);
// Set body content
this.displayDoc.body.className = 'page-content';
this.displayDoc.body.innerHTML = html;
// Copy styles from page head and set custom styles for editor
this.loadStylesIntoDisplay();
}
loadStylesIntoDisplay() {
if (this.displayStylesLoaded) return;
this.displayDoc.documentElement.classList.add('markdown-editor-display');
// Set display to be dark mode if parent is
if (document.documentElement.classList.contains('dark-mode')) {
this.displayDoc.documentElement.style.backgroundColor = '#222';
this.displayDoc.documentElement.classList.add('dark-mode');
}
this.displayDoc.head.innerHTML = '';
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
for (let style of styles) {
const copy = style.cloneNode(true);
this.displayDoc.head.appendChild(copy);
}
this.displayStylesLoaded = true;
this.display.innerHTML = html;
}
onMarkdownScroll(lineCount) {
const elems = this.displayDoc.body.children;
const elems = this.display.children;
if (elems.length <= lineCount) return;
const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
@@ -317,7 +281,7 @@ class MarkdownEditor {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let newLineContent = lineContent;
let newLineContent;
if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
@@ -333,9 +297,9 @@ class MarkdownEditor {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);
let newSelection = selection;
let newSelection;
let frontDiff = 0;
let endDiff = 0;
let endDiff;
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
@@ -445,10 +409,10 @@ class MarkdownEditor {
DrawIO.show(url,() => {
return Promise.resolve('');
}, (pngData) => {
}, (drawingData) => {
const data = {
image: pngData,
image: drawingData,
uploaded_to: Number(this.pageId),
};
@@ -462,7 +426,7 @@ class MarkdownEditor {
}
insertDrawing(image, originalCursor) {
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
const newText = DrawIO.buildDrawingContentHtml(image);
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
@@ -480,21 +444,22 @@ class MarkdownEditor {
DrawIO.show(drawioUrl, () => {
return DrawIO.load(drawingId);
}, (pngData) => {
}, (drawingData) => {
let data = {
image: pngData,
image: drawingData,
uploaded_to: Number(this.pageId),
};
window.$http.post("/images/drawio", data).then(resp => {
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
let newContent = this.cm.getValue().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
return newText;
}
return line;
const image = resp.data;
const newText = DrawIO.buildDrawingContentHtml(image);
const newContent = this.cm.getValue().split('\n').map(line => {
const isDrawing = line.includes(`drawio-diagram="${drawingId}"`);
return isDrawing ? newText : line;
}).join('\n');
this.cm.setValue(newContent);
this.cm.setCursor(cursorPos);
this.cm.focus();

View File

@@ -24,6 +24,8 @@ class PageEditor {
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
this.changelogInput = this.$refs.changelogInput;
this.changelogDisplay = this.$refs.changelogDisplay;
this.changeEditorButtons = this.$manyRefs.changeEditor || [];
this.switchDialogContainer = this.$refs.switchDialog;
// Translations
this.draftText = this.$opts.draftText;
@@ -72,6 +74,9 @@ class PageEditor {
// Draft Controls
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
// Change editor controls
onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
}
setInitialFocus() {
@@ -113,17 +118,21 @@ class PageEditor {
data.markdown = this.editorMarkdown;
}
let didSave = false;
try {
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
if (!this.isNewDraft) {
this.toggleDiscardDraftVisibility(true);
}
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
this.shownWarningsCache.add(resp.data.warning);
}
didSave = true;
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
try {
@@ -134,6 +143,7 @@ class PageEditor {
window.$events.emit('error', this.autosaveFailText);
}
return didSave;
}
draftNotifyChange(text) {
@@ -185,6 +195,18 @@ class PageEditor {
this.discardDraftWrap.classList.toggle('hidden', !show);
}
async changeEditor(event) {
event.preventDefault();
const link = event.target.closest('a').href;
const dialog = this.switchDialogContainer.components['confirm-dialog'];
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
if (saved && confirmed) {
window.location = link;
}
}
}
export default PageEditor;

View File

@@ -34,7 +34,7 @@ class Popup {
}
hide(onComplete = null) {
fadeOut(this.container, 240, onComplete);
fadeOut(this.container, 120, onComplete);
if (this.onkeyup) {
window.removeEventListener('keyup', this.onkeyup);
this.onkeyup = null;
@@ -45,7 +45,7 @@ class Popup {
}
show(onComplete = null, onHide = null) {
fadeIn(this.container, 240, onComplete);
fadeIn(this.container, 120, onComplete);
this.onkeyup = (event) => {
if (event.key === 'Escape') {

View File

@@ -43,6 +43,8 @@ function drawReceive(event) {
drawEventSave(message);
} else if (message.event === 'export') {
drawEventExport(message);
} else if (message.event === 'configure') {
drawEventConfigure();
}
}
@@ -53,7 +55,7 @@ function drawEventExport(message) {
}
function drawEventSave(message) {
drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
drawPostMessage({action: 'export', format: 'xmlsvg', xml: message.xml, spin: 'Updating drawing'});
}
function drawEventInit() {
@@ -63,6 +65,12 @@ function drawEventInit() {
});
}
function drawEventConfigure() {
const config = {};
window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});
drawPostMessage({action: 'configure', config});
}
function drawEventClose() {
window.removeEventListener('message', drawReceive);
if (iFrame) document.body.removeChild(iFrame);
@@ -88,7 +96,21 @@ async function upload(imageData, pageUploadedToId) {
*/
async function load(drawingId) {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
return resp.data.content;
}
export default {show, close, upload, load};
function buildDrawingContentHtml(drawing) {
const isSvg = drawing.url.split('.').pop().toLowerCase() === 'svg';
const image = `<img src="${drawing.url}">`;
const embed = `<embed src="${drawing.url}" type="image/svg+xml">`;
return `<div drawio-diagram="${drawing.id}">${isSvg ? embed : image}</div>`
}
function buildDrawingContentNode(drawing) {
const div = document.createElement('div');
div.innerHTML = buildDrawingContentHtml(drawing);
return div.children[0];
}
export default {show, close, upload, load, buildDrawingContentHtml, buildDrawingContentNode};

View File

@@ -2,6 +2,7 @@ import {register as registerShortcuts} from "./shortcuts";
import {listen as listenForCommonEvents} from "./common-events";
import {scrollToQueryString} from "./scrolling";
import {listenForDragAndPaste} from "./drop-paste-handling";
import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars";
import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
@@ -9,6 +10,7 @@ import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
import {getPlugin as getAboutPlugin} from "./plugins-about";
import {getPlugin as getDetailsPlugin} from "./plugins-details";
import {getPlugin as getTasklistPlugin} from "./plugins-tasklist";
const style_formats = [
{title: "Large Header", format: "h2", preview: 'color: blue;'},
@@ -58,48 +60,6 @@ function file_picker_callback(callback, value, meta) {
}
/**
* @param {WysiwygConfigOptions} options
* @return {{toolbar: string, groupButtons: Object<string, Object>}}
*/
function buildToolbar(options) {
const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
const groupButtons = {
formatoverflow: {
icon: 'more-drawer',
tooltip: 'More',
items: 'strikethrough superscript subscript inlinecode removeformat'
},
listoverflow: {
icon: 'more-drawer',
tooltip: 'More',
items: 'outdent indent'
},
insertoverflow: {
icon: 'more-drawer',
tooltip: 'More',
items: 'hr codeeditor drawio media details'
}
};
const toolbar = [
'undo redo',
'styleselect',
'bold italic underline forecolor backcolor formatoverflow',
'alignleft aligncenter alignright alignjustify',
'bullist numlist listoverflow',
textDirPlugins,
'link table imagemanager-insert insertoverflow',
'code about fullscreen'
];
return {
toolbar: toolbar.filter(row => Boolean(row)).join(' | '),
groupButtons,
};
}
/**
* @param {WysiwygConfigOptions} options
* @return {string}
@@ -122,6 +82,7 @@ function gatherPlugins(options) {
"imagemanager",
"about",
"details",
"tasklist",
options.textDirection === 'rtl' ? 'directionality' : '',
];
@@ -130,6 +91,7 @@ function gatherPlugins(options) {
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
window.tinymce.PluginManager.add('about', getAboutPlugin(options));
window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
if (options.drawioUrl) {
window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@@ -152,6 +114,23 @@ function fetchCustomHeadContent() {
return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
}
/**
* Setup a serializer filter for <br> tags to ensure they're not rendered
* within code blocks and that we use newlines there instead.
* @param {Editor} editor
*/
function setupBrFilter(editor) {
editor.serializer.addNodeFilter('br', function(nodes) {
for (const node of nodes) {
if (node.parent && node.parent.name === 'code') {
const newline = new tinymce.html.Node.create('#text');
newline.value = '\n';
node.replace(newline);
}
}
});
}
/**
* @param {WysiwygConfigOptions} options
* @return {function(Editor)}
@@ -169,6 +148,10 @@ function getSetupCallback(options) {
window.editor = editor;
});
editor.on('PreInit', () => {
setupBrFilter(editor);
});
function editorChange() {
const content = editor.getContent();
if (options.darkMode) {
@@ -218,8 +201,6 @@ export function build(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);
// Build toolbar content
const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
// BookStack Version
const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
@@ -247,7 +228,7 @@ export function build(options) {
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*]',
extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked]',
automatic_uploads: false,
custom_elements: 'doc-root,code-block',
valid_children: [
@@ -261,7 +242,7 @@ export function build(options) {
plugins: gatherPlugins(options),
imagetools_toolbar: 'imageoptions',
contextmenu: false,
toolbar: toolbar,
toolbar: getPrimaryToolbar(options),
content_style: getContentStyle(options),
style_formats,
style_formats_merge: false,
@@ -281,9 +262,7 @@ export function build(options) {
head.innerHTML += fetchCustomHeadContent();
},
setup(editor) {
for (const [key, config] of Object.entries(toolBarGroupButtons)) {
editor.ui.registry.addGroupToolbarButton(key, config);
}
registerAdditionalToolbars(editor, options);
getSetupCallback(options)(editor);
},
};

View File

@@ -97,11 +97,18 @@ function defineCodeBlockCustomElement(editor) {
}
this.cleanChildContent();
const content = this.getContent();
const lines = content.split('\n').length;
const height = (lines * 19.2) + 18 + 24;
this.style.height = `${height}px`;
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
const renderCodeMirror = (Code) => {
this.cm = Code.wysiwygView(container, this.getContent(), this.getLanguage());
this.cm = Code.wysiwygView(container, content, this.getLanguage());
Code.updateLayout(this.cm);
setTimeout(() => {
this.style.height = null;
}, 1);
};
window.importVersioned('code').then((Code) => {

View File

@@ -1,4 +1,5 @@
import DrawIO from "../services/drawio";
import {build} from "./config";
let pageEditor = null;
let currentNode = null;
@@ -15,15 +16,14 @@ function isDrawing(node) {
function showDrawingManager(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
// Show image manager
window.ImageManager.show(function (image) {
if (selectedNode) {
let imgElem = selectedNode.querySelector('img');
pageEditor.dom.setAttrib(imgElem, 'src', image.url);
pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
pageEditor.dom.replace(buildDrawingNode(image), selectedNode);
} else {
let imgHTML = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
pageEditor.insertContent(imgHTML);
const drawingHtml = DrawIO.buildDrawingContentHtml(image);
pageEditor.insertContent(drawingHtml);
}
}, 'drawio');
}
@@ -34,7 +34,14 @@ function showDrawingEditor(mceEditor, selectedNode = null) {
DrawIO.show(options.drawioUrl, drawingInit, updateContent);
}
async function updateContent(pngData) {
function buildDrawingNode(drawing) {
const drawingEl = DrawIO.buildDrawingContentNode(drawing);
drawingEl.setAttribute('contenteditable', 'false');
drawingEl.setAttribute('data-ephox-embed-iri', 'true');
return drawingEl;
}
async function updateContent(drawingData) {
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');
@@ -50,11 +57,9 @@ async function updateContent(pngData) {
// Handle updating an existing image
if (currentNode) {
DrawIO.close();
let imgElem = currentNode.querySelector('img');
try {
const img = await DrawIO.upload(pngData, options.pageId);
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
const img = await DrawIO.upload(drawingData, options.pageId);
pageEditor.dom.replace(buildDrawingNode(img), currentNode);
} catch (err) {
handleUploadError(err);
}
@@ -62,12 +67,11 @@ async function updateContent(pngData) {
}
setTimeout(async () => {
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" alt="Loading" id="${id}"></div>`);
DrawIO.close();
try {
const img = await DrawIO.upload(pngData, options.pageId);
pageEditor.dom.setAttrib(id, 'src', img.url);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
const img = await DrawIO.upload(drawingData, options.pageId);
pageEditor.dom.replace(buildDrawingNode(img), pageEditor.dom.get(id).parentNode);
} catch (err) {
pageEditor.dom.remove(id);
handleUploadError(err);
@@ -86,7 +90,6 @@ function drawingInit() {
}
/**
*
* @param {WysiwygConfigOptions} providedOptions
* @return {function(Editor, string)}
*/
@@ -130,14 +133,28 @@ export function getPlugin(providedOptions) {
showDrawingEditor(editor, selectedNode);
});
editor.on('SetContent', function () {
const drawings = editor.$('body > div[drawio-diagram]');
if (!drawings.length) return;
editor.on('PreInit', () => {
editor.parser.addNodeFilter('div', function(nodes) {
for (const node of nodes) {
if (node.attr('drawio-diagram')) {
// Set content editable to be false to prevent direct editing of child content.
node.attr('contenteditable', 'false');
// Set this attribute to prevent drawing contents being parsed as media embeds
// to avoid contents being replaced with placeholder images.
// TinyMCE embed plugin sources looks for this attribute in its logic.
node.attr('data-ephox-embed-iri', 'true');
}
}
});
editor.undoManager.transact(function () {
drawings.each((index, elem) => {
elem.setAttribute('contenteditable', 'false');
});
editor.serializer.addNodeFilter('div', function(nodes) {
for (const node of nodes) {
// Clean up content attributes
if (node.attr('drawio-diagram')) {
node.attr('contenteditable', null);
node.attr('data-ephox-embed-iri', null);
}
}
});
});

View File

@@ -0,0 +1,171 @@
/**
* @param {Editor} editor
* @param {String} url
*/
function register(editor, url) {
// Tasklist UI buttons
editor.ui.registry.addIcon('tasklist', '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22,8c0-0.55-0.45-1-1-1h-7c-0.55,0-1,0.45-1,1s0.45,1,1,1h7C21.55,9,22,8.55,22,8z M13,16c0,0.55,0.45,1,1,1h7 c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1h-7C13.45,15,13,15.45,13,16z M10.47,4.63c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25 c-0.39,0.39-1.02,0.39-1.42,0L2.7,8.16c-0.39-0.39-0.39-1.02,0-1.41c0.39-0.39,1.02-0.39,1.41,0l1.42,1.42l3.54-3.54 C9.45,4.25,10.09,4.25,10.47,4.63z M10.48,12.64c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25c-0.39,0.39-1.02,0.39-1.42,0L2.7,16.16 c-0.39-0.39-0.39-1.02,0-1.41s1.02-0.39,1.41,0l1.42,1.42l3.54-3.54C9.45,12.25,10.09,12.25,10.48,12.64L10.48,12.64z"/></svg>');
editor.ui.registry.addToggleButton('tasklist', {
tooltip: 'Task list',
icon: 'tasklist',
active: false,
onAction(api) {
if (api.isActive()) {
editor.execCommand('RemoveList');
} else {
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {
class: 'task-list-item',
},
'list-style-type': 'tasklist',
});
}
},
onSetup(api) {
editor.on('NodeChange', event => {
const parentListEl = event.parents.find(el => el.nodeName === 'LI');
const inList = parentListEl && parentListEl.classList.contains('task-list-item');
api.setActive(inList);
});
}
});
// Tweak existing bullet list button active state to not be active
// when we're in a task list.
const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
existingBullListButton.onSetup = function(api) {
editor.on('NodeChange', event => {
const parentList = event.parents.find(el => el.nodeName === 'LI');
const inTaskList = parentList && parentList.classList.contains('task-list-item');
const inUlList = parentList && parentList.parentNode.nodeName === 'UL';
api.setActive(inUlList && !inTaskList);
});
};
existingBullListButton.onAction = function() {
// Cheeky hack to prevent list toggle action treating tasklists as normal
// unordered lists which would unwrap the list on toggle from tasklist to bullet list.
// Instead we quickly jump through an ordered list first if we're within a tasklist.
if (elementWithinTaskList(editor.selection.getNode())) {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null}
});
}
editor.execCommand('InsertUnorderedList', null, {
'list-item-attributes': {class: null}
});
};
// Tweak existing number list to not allow classes on child items
const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
existingNumListButton.onAction = function() {
editor.execCommand('InsertOrderedList', null, {
'list-item-attributes': {class: null}
});
};
// Setup filters on pre-init
editor.on('PreInit', () => {
editor.parser.addNodeFilter('li', function(nodes) {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
parseTaskListNode(node);
}
}
});
editor.serializer.addNodeFilter('li', function(nodes) {
for (const node of nodes) {
if (node.attributes.map.class === 'task-list-item') {
serializeTaskListNode(node);
}
}
});
});
// Handle checkbox click in editor
editor.on('click', function(event) {
const clickedEl = event.target;
if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
handleTaskListItemClick(event, clickedEl, editor);
event.preventDefault();
}
});
}
/**
* @param {Element} element
* @return {boolean}
*/
function elementWithinTaskList(element) {
const listEl = element.closest('li');
return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
}
/**
* @param {MouseEvent} event
* @param {Element} clickedEl
* @param {Editor} editor
*/
function handleTaskListItemClick(event, clickedEl, editor) {
const bounds = clickedEl.getBoundingClientRect();
const withinBounds = event.clientX <= bounds.right
&& event.clientX >= bounds.left
&& event.clientY >= bounds.top
&& event.clientY <= bounds.bottom;
// Outside of the task list item bounds mean we're probably clicking the pseudo-element.
if (!withinBounds) {
editor.undoManager.transact(() => {
if (clickedEl.hasAttribute('checked')) {
clickedEl.removeAttribute('checked');
} else {
clickedEl.setAttribute('checked', 'checked');
}
});
}
}
/**
* @param {AstNode} node
*/
function parseTaskListNode(node) {
// Force task list item class
node.attr('class', 'task-list-item');
// Copy checkbox status and remove checkbox within editor
for (const child of node.children()) {
if (child.name === 'input') {
if (child.attr('checked') === 'checked') {
node.attr('checked', 'checked');
}
child.remove();
}
}
}
/**
* @param {AstNode} node
*/
function serializeTaskListNode(node) {
// Get checked status and clean it from list node
const isChecked = node.attr('checked') === 'checked';
node.attr('checked', null);
const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
if (isChecked) {
inputAttrs.checked = 'checked';
}
// Create & insert checkbox input element
const checkbox = new tinymce.html.Node.create('input', inputAttrs);
checkbox.shortEnded = true;
node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
}
/**
* @param {WysiwygConfigOptions} options
* @return {register}
*/
export function getPlugin(options) {
return register;
}

View File

@@ -39,4 +39,19 @@ export function register(editor) {
editor.formatter.apply('callout' + newFormat);
});
// Link selector shortcut
editor.shortcuts.add('meta+shift+K', '', function() {
window.EntitySelectorPopup.show(function(entity) {
if (editor.selection.isCollapsed()) {
editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));
} else {
editor.formatter.apply('link', {href: entity.link});
}
editor.selection.collapse(false);
editor.focus();
})
});
}

View File

@@ -0,0 +1,64 @@
/**
* @param {WysiwygConfigOptions} options
* @return {String}
*/
export function getPrimaryToolbar(options) {
const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
const toolbar = [
'undo redo',
'styleselect',
'bold italic underline forecolor backcolor formatoverflow',
'alignleft aligncenter alignright alignjustify',
'bullist numlist listoverflow',
textDirPlugins,
'link table imagemanager-insert insertoverflow',
'code about fullscreen'
];
return toolbar.filter(row => Boolean(row)).join(' | ');
}
/**
* @param {Editor} editor
*/
function registerPrimaryToolbarGroups(editor) {
editor.ui.registry.addGroupToolbarButton('formatoverflow', {
icon: 'more-drawer',
tooltip: 'More',
items: 'strikethrough superscript subscript inlinecode removeformat'
});
editor.ui.registry.addGroupToolbarButton('listoverflow', {
icon: 'more-drawer',
tooltip: 'More',
items: 'tasklist outdent indent'
});
editor.ui.registry.addGroupToolbarButton('insertoverflow', {
icon: 'more-drawer',
tooltip: 'More',
items: 'hr codeeditor drawio media details'
});
}
/**
* @param {Editor} editor
*/
function registerLinkContextToolbar(editor) {
editor.ui.registry.addContextToolbar('linkcontexttoolbar', {
predicate(node) {
return node.closest('a') !== null;
},
position: 'node',
scope: 'node',
items: 'link unlink openlink'
});
}
/**
* @param {Editor} editor
* @param {WysiwygConfigOptions} options
*/
export function registerAdditionalToolbars(editor, options) {
registerPrimaryToolbarGroups(editor);
registerLinkContextToolbar(editor);
}

View File

@@ -7,61 +7,61 @@ return [
// Pages
'page_create' => 'تم إنشاء صفحة',
'page_create_notification' => 'Page successfully created',
'page_create_notification' => 'تم إنشاء الصفحة بنجاح',
'page_update' => 'تم تحديث الصفحة',
'page_update_notification' => 'Page successfully updated',
'page_update_notification' => 'تم تحديث الصفحة بنجاح',
'page_delete' => 'تم حذف الصفحة',
'page_delete_notification' => 'Page successfully deleted',
'page_delete_notification' => 'تم حذف الصفحة بنجاح',
'page_restore' => 'تمت استعادة الصفحة',
'page_restore_notification' => 'Page successfully restored',
'page_restore_notification' => 'تمت استعادة الصفحة بنجاح',
'page_move' => 'تم نقل الصفحة',
// Chapters
'chapter_create' => 'تم إنشاء فصل',
'chapter_create_notification' => 'Chapter successfully created',
'chapter_create_notification' => 'تم إنشاء الفصل بنجاح',
'chapter_update' => 'تم تحديث الفصل',
'chapter_update_notification' => 'Chapter successfully updated',
'chapter_update_notification' => 'تم تحديث الفصل بنجاح',
'chapter_delete' => 'تم حذف الفصل',
'chapter_delete_notification' => 'Chapter successfully deleted',
'chapter_delete_notification' => 'تم حذف الفصل بنجاح',
'chapter_move' => 'تم نقل الفصل',
// Books
'book_create' => 'تم إنشاء كتاب',
'book_create_notification' => 'Book successfully created',
'book_create_notification' => 'تم إنشاء الكتاب بنجاح',
'book_update' => 'تم تحديث الكتاب',
'book_update_notification' => 'Book successfully updated',
'book_update_notification' => 'تم تحديث الكتاب بنجاح',
'book_delete' => 'تم حذف الكتاب',
'book_delete_notification' => 'Book successfully deleted',
'book_delete_notification' => 'تم حذف الكتاب بنجاح',
'book_sort' => 'تم سرد الكتاب',
'book_sort_notification' => 'Book successfully re-sorted',
'book_sort_notification' => 'تم إعادة فرز الكتاب بنجاح',
// Bookshelves
'bookshelf_create' => 'created bookshelf',
'bookshelf_create_notification' => 'Bookshelf successfully created',
'bookshelf_create' => 'تم إنشاء رف كتب',
'bookshelf_create_notification' => 'تم إنشاء الرف بنجاح',
'bookshelf_update' => 'تم تحديث الرف',
'bookshelf_update_notification' => 'Bookshelf successfully updated',
'bookshelf_update_notification' => 'تم تحديث الرف بنجاح',
'bookshelf_delete' => 'تم تحديث الرف',
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
'favourite_add_notification' => 'تم إضافة ":name" إلى المفضلة لديك',
'favourite_remove_notification' => 'تم إزالة ":name" من المفضلة لديك',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'تم تكوين طريقة متعددة العوامل بنجاح',
'mfa_remove_method_notification' => 'تمت إزالة طريقة متعددة العوامل بنجاح',
// Webhooks
'webhook_create' => 'created webhook',
'webhook_create_notification' => 'Webhook successfully created',
'webhook_update' => 'updated webhook',
'webhook_update_notification' => 'Webhook successfully updated',
'webhook_delete' => 'deleted webhook',
'webhook_delete_notification' => 'Webhook successfully deleted',
'webhook_create' => 'تم إنشاء webhook',
'webhook_create_notification' => 'تم إنشاء Webhook بنجاح',
'webhook_update' => 'تم تحديث webhook',
'webhook_update_notification' => 'تم تحديث Webhook بنجاح',
'webhook_delete' => 'حذف webhook',
'webhook_delete_notification' => 'تم حذف Webhook بنجاح',
// Users
'user_update_notification' => 'User successfully updated',
'user_delete_notification' => 'User successfully removed',
'user_update_notification' => 'تم تحديث المستخدم بنجاح',
'user_delete_notification' => 'تم إزالة المستخدم بنجاح',
// Other
'commented_on' => 'تم التعليق',

View File

@@ -24,6 +24,7 @@ return [
'width' => 'العرض',
'height' => 'الارتفاع',
'More' => 'المزيد',
'select' => 'Select...',
// Toolbar
'formats' => 'التنسيقات',
@@ -52,9 +53,10 @@ return [
'align_left' => 'محاذاة لليسار',
'align_center' => 'محاذاة بالمنتصف',
'align_right' => 'مُحاذاة لليمين',
'align_justify' => 'ضبط المحاذاة',
'align_justify' => 'Justify',
'list_bullet' => 'قائمة نقاط',
'list_numbered' => 'قائمة مرقمة',
'list_task' => 'Task list',
'indent_increase' => 'زيادة البادئة',
'indent_decrease' => 'إنقاص البادئة',
'table' => 'جدول',
@@ -91,7 +93,10 @@ return [
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
@@ -119,6 +124,16 @@ return [
'caption' => 'الوصف',
'show_caption' => 'إظهار الوصف',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
@@ -139,12 +154,14 @@ return [
'toggle_label' => 'Toggle label',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',

View File

@@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'تم خفظ المسودة في ',
'pages_edit_delete_draft' => 'حذف المسودة',
'pages_edit_discard_draft' => 'التخلص من المسودة',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'تثبيت سجل التعديل',
'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',
'pages_edit_enter_changelog' => 'أدخل سجل التعديل',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'حفظ الصفحة',
'pages_title' => 'عنوان الصفحة',
'pages_name' => 'اسم الصفحة',
@@ -225,6 +235,7 @@ return [
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'مراجعة #:id',
'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'سجل التعديل',
'pages_revisions_changes' => 'التعديلات',
'pages_revisions_current' => 'النسخة الحالية',

View File

@@ -10,6 +10,8 @@ return [
'settings' => 'الإعدادات',
'settings_save' => 'حفظ الإعدادات',
'settings_save_success' => 'تم حفظ الإعدادات',
'system_version' => 'System Version',
'categories' => 'Categories',
// App Settings
'app_customization' => 'تخصيص',
@@ -25,8 +27,8 @@ return [
'app_secure_images' => 'تفعيل حماية أكبر لرفع الصور؟',
'app_secure_images_toggle' => 'لمزيد من الحماية',
'app_secure_images_desc' => 'لتحسين أداء النظام, ستكون جميع الصور متاحة للعامة. هذا الخيار يضيف سلسلة من الحروف والأرقام العشوائية صعبة التخمين إلى رابط الصورة. الرجاء التأكد من تعطيل فهرسة المسارات لمنع الوصول السهل.',
'app_editor' => 'محرر الصفحة',
'app_editor_desc' => 'الرجاء اختيار محرر النص الذي سيستخدم من قبل جميع المستخدمين لتحرير الصفحات.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Custom HTML head content',
'app_custom_html_desc' => 'سيتم إدراج أي محتوى مضاف هنا في الجزء السفلي من قسم <head> من كل صفحة. هذا أمر مفيد لتجاوز الأنماط أو إضافة رمز التحليل.',
'app_custom_html_disabled_notice' => 'تم تعطيل محتوى HTML الرئيسي المخصص في صفحة الإعدادات هذه لضمان عكس أي تغييرات متتالية.',
@@ -150,6 +152,7 @@ return [
'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API',
'role_manage_settings' => 'إدارة إعدادات التطبيق',
'role_export_content' => 'Export content',
'role_editor_change' => 'Change page editor',
'role_asset' => 'أذونات الأصول',
'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.',
'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.',
@@ -275,6 +278,8 @@ return [
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti keel',
'eu' => 'Euskara',
'fa' => 'فارسی',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@@ -7,63 +7,63 @@ return [
// Pages
'page_create' => 'създадена страница',
'page_create_notification' => 'Page successfully created',
'page_create_notification' => 'Страницата е създадена успешно',
'page_update' => 'обновена страница',
'page_update_notification' => 'Page successfully updated',
'page_update_notification' => 'Страницата е обновена успешно',
'page_delete' => 'изтрита страница',
'page_delete_notification' => 'Page successfully deleted',
'page_delete_notification' => 'Страницата е изтрита успешно',
'page_restore' => 'възстановена страница',
'page_restore_notification' => 'Page successfully restored',
'page_restore_notification' => 'Страницата е възстановена успешно',
'page_move' => 'преместена страница',
// Chapters
'chapter_create' => 'създадена страница',
'chapter_create_notification' => 'Chapter successfully created',
'chapter_create_notification' => 'Главата е добавена успешно',
'chapter_update' => 'обновена глава',
'chapter_update_notification' => 'Chapter successfully updated',
'chapter_update_notification' => 'Главата е обновена успешно',
'chapter_delete' => 'изтрита глава',
'chapter_delete_notification' => 'Chapter successfully deleted',
'chapter_delete_notification' => 'Главата е изтрита успешно',
'chapter_move' => 'преместена глава',
// Books
'book_create' => 'създадена книга',
'book_create_notification' => 'Book successfully created',
'book_create_notification' => 'Книгата е създадена успешно',
'book_update' => 'обновена книга',
'book_update_notification' => 'Book successfully updated',
'book_update_notification' => 'Книгата е обновена успешно',
'book_delete' => 'изтрита книга',
'book_delete_notification' => 'Book successfully deleted',
'book_delete_notification' => 'Книгата е изтрита успешно',
'book_sort' => 'сортирана книга',
'book_sort_notification' => 'Book successfully re-sorted',
'book_sort_notification' => 'Книгата е преподредена успешно',
// Bookshelves
'bookshelf_create' => 'created bookshelf',
'bookshelf_create_notification' => 'Bookshelf successfully created',
'bookshelf_create' => 'създаден рафт',
'bookshelf_create_notification' => 'Рафтът е създаден успешно',
'bookshelf_update' => 'обновен рафт',
'bookshelf_update_notification' => 'Bookshelf successfully updated',
'bookshelf_update_notification' => 'Рафтът е обновен успешно',
'bookshelf_delete' => 'изтрит рафт',
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
'bookshelf_delete_notification' => 'Рафтът е изтрит успешно',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
'favourite_add_notification' => '":name" е добавен към любими успешно',
'favourite_remove_notification' => '":name" е премахнат от любими успешно',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Многофакторният метод е конфигуриран успешно',
'mfa_remove_method_notification' => 'Многофакторният метод е премахнат успешно',
// Webhooks
'webhook_create' => 'created webhook',
'webhook_create_notification' => 'Webhook successfully created',
'webhook_update' => 'updated webhook',
'webhook_update_notification' => 'Webhook successfully updated',
'webhook_delete' => 'deleted webhook',
'webhook_delete_notification' => 'Webhook successfully deleted',
'webhook_create' => 'създадена уебкука',
'webhook_create_notification' => 'Уебкуката е създадена успешно',
'webhook_update' => 'обновена уебкука',
'webhook_update_notification' => 'Уебкуката е обновена успешно',
'webhook_delete' => 'изтрита уебкука',
'webhook_delete_notification' => 'Уебкуката е изтрита успешно',
// Users
'user_update_notification' => 'User successfully updated',
'user_delete_notification' => 'User successfully removed',
'user_update_notification' => 'Потребителят е обновен успешно',
'user_delete_notification' => 'Потребителят е премахнат успешно',
// Other
'commented_on' => 'коментирано на',
'permissions_update' => 'updated permissions',
'permissions_update' => 'обновени права',
];

View File

@@ -17,23 +17,23 @@ return [
'logout' => 'Изход',
'name' => 'Име',
'username' => 'Потребител',
'username' => 'Потребителско име',
'email' => 'Имейл',
'password' => 'Парола',
'password_confirm' => 'Потвърди паролата',
'password_hint' => 'Must be at least 8 characters',
'password_hint' => 'Трябва да бъде поне 8 символа',
'forgot_password' => 'Забравена парола?',
'remember_me' => 'Запомни ме',
'ldap_email_hint' => 'Моля въведете емейл, който да използвате за дадения акаунт.',
'ldap_email_hint' => 'Моля въведете емейл, който да използвате за дадения профил.',
'create_account' => 'Създай Акаунт',
'already_have_account' => 'Вече имате акаунт?',
'already_have_account' => 'Вече имате профил?',
'dont_have_account' => 'Нямате акаунт?',
'social_login' => 'Влизане по друг начин',
'social_registration' => 'Регистрация по друг начин',
'social_registration_text' => 'Регистрация и влизане използвайки друг начин.',
'social_registration_text' => 'Регистрация и вписване чрез друга услуга.',
'register_thanks' => 'Благодарим Ви за регистрацията!',
'register_confirm' => 'Моля проверете своя емейл и натиснете върху бутона за потвърждение, за да влезете в :appName.',
'register_confirm' => 'Моля, провери своя имейл адрес и натисни бутона за потвърждение, за да достъпиш :appName.',
'registrations_disabled' => 'Регистрациите към момента са забранени',
'registration_email_domain_invalid' => 'Този емейл домейн към момента няма достъп до приложението',
'register_success' => 'Благодарим Ви за регистрацията! В момента сте регистриран и сте вписани в приложението.',
@@ -41,11 +41,11 @@ return [
// Password Reset
'reset_password' => 'Нулиране на паролата',
'reset_password_send_instructions' => 'Въведете емейла си и ще ви бъде изпратен емейл с линк за нулиране на паролата.',
'reset_password_send_button' => 'Изпращане на линк за нулиране',
'reset_password_send_button' => 'Изпращане на линк за възстановяване',
'reset_password_sent' => 'Линк за нулиране на паролата ще Ви бъде изпратен на :email, ако емейлът Ви бъде открит в системата.',
'reset_password_success' => 'Паролата Ви е променена успешно.',
'email_reset_subject' => 'Възстановете паролата си за :appName',
'email_reset_text' => 'Вие получихте този емейл, защото поискахте вашата парола да бъде занулена.',
'email_reset_subject' => 'Възстанови паролата си за :appName',
'email_reset_text' => 'Вие получихте този имейл, защото поискахте Вашата парола да бъде възстановена.',
'email_reset_not_requested' => 'Ако Вие не сте поискали зануляването на паролата, няма нужда от други действия.',
// Email Confirmation
@@ -54,7 +54,7 @@ return [
'email_confirm_text' => 'Моля, потвърдете вашия имейл адрес, като следвате връзката по-долу:',
'email_confirm_action' => 'Потвърдете имейл',
'email_confirm_send_error' => 'Нужно ви е потвърждение чрез емейл, но системата не успя да го изпрати. Моля свържете се с администратора, за да проверите дали вашият емейл адрес е конфигуриран правилно.',
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
'email_confirm_success' => 'Имейлът ти е потвърден! Вече би трябвало да можеш да се впишеш с този имейл адрес.',
'email_confirm_resent' => 'Беше изпратен имейл с потвърждение, Моля, проверете кутията си.',
'email_not_confirmed' => 'Имейл адресът не е потвърден',
@@ -71,40 +71,40 @@ return [
'user_invite_page_welcome' => 'Добре дошли в :appName!',
'user_invite_page_text' => 'За да финализирате вашият акаунт и да получите достъп трябва да определите парола, която да бъде използвана за следващия влизания в :appName.',
'user_invite_page_confirm_button' => 'Потвърди паролата',
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
'user_invite_success_login' => 'Паролата е настроена, вече можеш да се впишеш с новата парола, за да достъпиш :appName!',
// Multi-factor Authentication
'mfa_setup' => 'Setup Multi-Factor Authentication',
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'mfa_setup_configured' => 'Already configured',
'mfa_setup_reconfigure' => 'Reconfigure',
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
'mfa_setup_action' => 'Setup',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_option_totp_title' => 'Mobile App',
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Backup Codes',
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
'mfa_gen_backup_codes_download' => 'Download Codes',
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
'mfa_gen_totp_title' => 'Mobile App Setup',
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
'mfa_gen_totp_verify_setup' => 'Verify Setup',
'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
'mfa_verify_access' => 'Verify Access',
'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
'mfa_verify_no_methods' => 'No Methods Configured',
'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
'mfa_verify_use_totp' => 'Verify using a mobile app',
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
'mfa_verify_backup_code' => 'Backup Code',
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
'mfa_setup' => 'Настрой многофакторно удостоверяване',
'mfa_setup_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.',
'mfa_setup_configured' => 'Вече е конфигурирано',
'mfa_setup_reconfigure' => 'Преконфигурирай',
'mfa_setup_remove_confirmation' => 'Сигурен ли си, че желаеш да премахнеш този метод за многофакторно удостоверяване?',
'mfa_setup_action' => 'Настройка',
'mfa_backup_codes_usage_limit_warning' => 'Имаш по-малко от 5 останали резервни кода. Генерирай и съхрани нов набор, преди тези да са свършили, за да избегнеш да останеш без достъп до профила си.',
'mfa_option_totp_title' => 'Мобилно приложение',
'mfa_option_totp_desc' => 'За да използваш многофакторно удостоверяване, ще ти трябва мобилно приложение, което поддържа временни еднократни пароли (TOTP), като например Google Authenticator, Authy или Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Резервни кодове',
'mfa_option_backup_codes_desc' => 'Запази на сигурно място набор от еднократни резервни кодове, с които можеш да устовериш идентичността си.',
'mfa_gen_confirm_and_enable' => 'Потвърди и включи',
'mfa_gen_backup_codes_title' => 'Настройка на резервни кодове',
'mfa_gen_backup_codes_desc' => 'Запази този лист с кодове на сигурно място. Когато достъпваш системата, ще можеш да използваш един от тези кодове като вторичен механизъм за удостоверяване.',
'mfa_gen_backup_codes_download' => 'Изтегли кодовете',
'mfa_gen_backup_codes_usage_warning' => 'Всеки код може да бъде използван само веднъж',
'mfa_gen_totp_title' => 'Настройка на мобилно приложение',
'mfa_gen_totp_desc' => 'За да използваш многофакторно удостоверяване, ще ти трябва мобилно приложение, което поддържа временни еднократни пароли (TOTP), като например Google Authenticator, Authy или Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'За да започнеш, сканирай QR кода отдолу с предпочитано от теб приложение.',
'mfa_gen_totp_verify_setup' => 'Потвърди настройката',
'mfa_gen_totp_verify_setup_desc' => 'Потвърди, че всичко работи, като в кутията отдолу въведеш код, генериран от твоето приложение за удостоверяване:',
'mfa_gen_totp_provide_code_here' => 'Въведи тук кода, генериран от мобилното ти приложение',
'mfa_verify_access' => 'Потвърди достъпа',
'mfa_verify_access_desc' => 'Твоят потребителски профил изисква да потвърдиш идентичността си чрез допълнително ниво проверка преди да получиш достъп. Потвърди чрез един от конфигурираните методи, за да продължиш.',
'mfa_verify_no_methods' => 'Няма конфигурирани методи',
'mfa_verify_no_methods_desc' => 'Няма намерени методи за многофакторно удостоверяване за твоя профил. Ще трябва да настроиш поне един метод, преди да получиш достъп.',
'mfa_verify_use_totp' => 'Потвърди чрез мобилно приложение',
'mfa_verify_use_backup_codes' => 'Потвърди чрез резервен код',
'mfa_verify_backup_code' => 'Резервен код',
'mfa_verify_backup_code_desc' => 'Въведи един от останалите ти резервни кодове отдолу:',
'mfa_verify_backup_code_enter_here' => 'Въведи резервен код тук',
'mfa_verify_totp_desc' => 'Въведи кода, генериран от мобилното ти приложение, отдолу:',
'mfa_setup_login_notification' => 'Многофакторният метод е конфигуриран, моля да се впишете отново чрез конфигурирания метод.',
];

View File

@@ -39,14 +39,14 @@ return [
'reset' => 'Нулирай',
'remove' => 'Премахване',
'add' => 'Добави',
'configure' => 'Configure',
'configure' => 'Конфигурирай',
'fullscreen' => 'Пълен екран',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
'filter_active' => 'Active Filter:',
'filter_clear' => 'Clear Filter',
'favourite' => 'Добави в любими',
'unfavourite' => 'Премахни от любими',
'next' => 'Следващ',
'previous' => 'Предишен',
'filter_active' => 'Активен филтър:',
'filter_clear' => 'Изчисти филтъра',
// Sort Options
'sort_options' => 'Опции за сортиране',
@@ -54,7 +54,7 @@ return [
'sort_ascending' => 'Сортирай възходящо',
'sort_descending' => 'Низходящо сортиране',
'sort_name' => 'Име',
'sort_default' => 'Default',
'sort_default' => 'По подразбиране',
'sort_created_at' => 'Дата на създаване',
'sort_updated_at' => 'Дата на обновяване',
@@ -63,7 +63,7 @@ return [
'no_activity' => 'Няма активност за показване',
'no_items' => 'Няма налични артикули',
'back_to_top' => 'Върнете се в началото',
'skip_to_main_content' => 'Skip to main content',
'skip_to_main_content' => 'Прескочи към основното съдържание',
'toggle_details' => 'Активирай детайли',
'toggle_thumbnails' => 'Активирай миниатюри',
'details' => 'Подробности',
@@ -71,14 +71,14 @@ return [
'list_view' => 'Изглед списък',
'default' => 'Основен',
'breadcrumb' => 'Трасиране',
'status' => 'Status',
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
'status' => 'Статус',
'status_active' => 'Активен',
'status_inactive' => 'Неактивен',
'never' => 'Никога',
'none' => 'Няма',
// Header
'header_menu_expand' => 'Expand Header Menu',
'header_menu_expand' => 'Разшири заглавното меню',
'profile_menu' => 'Профил меню',
'view_profile' => 'Разглеждане на профил',
'edit_profile' => 'Редактиране на профила',
@@ -87,9 +87,9 @@ return [
// Layout tabs
'tab_info' => 'Информация',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_info_label' => 'Таб: Покажи вторична информация',
'tab_content' => 'Съдържание',
'tab_content_label' => 'Tab: Show Primary Content',
'tab_content_label' => 'Таб: Покажи първично съдържание',
// Email Content
'email_action_help' => 'Ако имате проблеми с бутона ":actionText" по-горе, копирайте и поставете URL адреса по-долу в уеб браузъра си:',

View File

@@ -29,6 +29,6 @@ return [
'code_editor' => 'Редактиране на кода',
'code_language' => 'Език на кода',
'code_content' => 'Съдържание на кода',
'code_session_history' => 'Session History',
'code_session_history' => 'История на сесиите',
'code_save' => 'Запази кода',
];

View File

@@ -7,148 +7,165 @@
*/
return [
// General editor terms
'general' => 'General',
'advanced' => 'Advanced',
'none' => 'None',
'cancel' => 'Cancel',
'save' => 'Save',
'close' => 'Close',
'undo' => 'Undo',
'redo' => 'Redo',
'left' => 'Left',
'center' => 'Center',
'right' => 'Right',
'top' => 'Top',
'middle' => 'Middle',
'bottom' => 'Bottom',
'width' => 'Width',
'height' => 'Height',
'More' => 'More',
'general' => 'Общи',
'advanced' => 'Разширени',
'none' => 'Няма',
'cancel' => 'Откажи',
'save' => 'Запази',
'close' => 'Затвори',
'undo' => 'Отмени',
'redo' => 'Преправи',
'left' => 'Вляво',
'center' => 'По средата',
'right' => 'Вдясно',
'top' => 'Отгоре',
'middle' => 'Среда',
'bottom' => 'Отдолу',
'width' => 'Широчина',
'height' => 'Височина',
'More' => 'Още',
'select' => 'Select...',
// Toolbar
'formats' => 'Formats',
'header_large' => 'Large Header',
'header_medium' => 'Medium Header',
'header_small' => 'Small Header',
'header_tiny' => 'Tiny Header',
'paragraph' => 'Paragraph',
'blockquote' => 'Blockquote',
'inline_code' => 'Inline code',
'callouts' => 'Callouts',
'callout_information' => 'Information',
'callout_success' => 'Success',
'callout_warning' => 'Warning',
'callout_danger' => 'Danger',
'bold' => 'Bold',
'italic' => 'Italic',
'underline' => 'Underline',
'strikethrough' => 'Strikethrough',
'superscript' => 'Superscript',
'subscript' => 'Subscript',
'text_color' => 'Text color',
'custom_color' => 'Custom color',
'remove_color' => 'Remove color',
'background_color' => 'Background color',
'align_left' => 'Align left',
'align_center' => 'Align center',
'align_right' => 'Align right',
'align_justify' => 'Align justify',
'list_bullet' => 'Bullet list',
'list_numbered' => 'Numbered list',
'indent_increase' => 'Increase indent',
'indent_decrease' => 'Decrease indent',
'table' => 'Table',
'insert_image' => 'Insert image',
'insert_image_title' => 'Insert/Edit Image',
'insert_link' => 'Insert/edit link',
'insert_link_title' => 'Insert/Edit Link',
'insert_horizontal_line' => 'Insert horizontal line',
'insert_code_block' => 'Insert code block',
'insert_drawing' => 'Insert/edit drawing',
'drawing_manager' => 'Drawing manager',
'insert_media' => 'Insert/edit media',
'insert_media_title' => 'Insert/Edit Media',
'clear_formatting' => 'Clear formatting',
'source_code' => 'Source code',
'source_code_title' => 'Source Code',
'fullscreen' => 'Fullscreen',
'image_options' => 'Image options',
'formats' => 'Формати',
'header_large' => 'Голяма заглавка',
'header_medium' => 'Средна заглавка',
'header_small' => 'Малка заглавка',
'header_tiny' => 'Миниатюрна заглавка',
'paragraph' => 'Параграф',
'blockquote' => 'Цитат',
'inline_code' => 'Вложен код',
'callouts' => 'Призиви',
'callout_information' => 'Информация',
'callout_success' => 'Успех',
'callout_warning' => 'Предупреждение',
'callout_danger' => 'Опасност',
'bold' => 'Удебелено',
'italic' => 'Наклонен',
'underline' => 'Подчертан',
'strikethrough' => 'Зачертан',
'superscript' => 'Горен индекс',
'subscript' => 'Долен индекс',
'text_color' => 'Цвят на текста',
'custom_color' => 'Цвят по избор',
'remove_color' => 'Премахване на цвят',
'background_color' => 'Фонов цвят',
'align_left' => 'Приравни вляво',
'align_center' => 'Приравни в центъра',
'align_right' => 'Приравни вдясно',
'align_justify' => 'Justify',
'list_bullet' => 'Списък',
'list_numbered' => 'Номериран списък',
'list_task' => 'Task list',
'indent_increase' => 'Увеличаване на отстъпа',
'indent_decrease' => 'Намаляване на отстъпа',
'table' => 'Таблица',
'insert_image' => 'Вмъкни изображение',
'insert_image_title' => 'Вмъкни/редактирай изображение',
'insert_link' => 'Вмъкни/редактирай връзка',
'insert_link_title' => 'Вмъкни/редактирай връзка',
'insert_horizontal_line' => 'Вмъкни хоризонтална линия',
'insert_code_block' => 'Въведи код',
'insert_drawing' => 'Вмъкни/редактирай рисунка',
'drawing_manager' => 'Управление на рисунките',
'insert_media' => 'Вмъкни/редактирай мултимедия',
'insert_media_title' => 'Вмъкни/редактирай мултимедия',
'clear_formatting' => 'Изчисти форматирането',
'source_code' => 'Изходен код',
'source_code_title' => 'Изходен код',
'fullscreen' => 'Цял екран',
'image_options' => 'Настройки на изображението',
// Tables
'table_properties' => 'Table properties',
'table_properties_title' => 'Table Properties',
'delete_table' => 'Delete table',
'insert_row_before' => 'Insert row before',
'insert_row_after' => 'Insert row after',
'delete_row' => 'Delete row',
'insert_column_before' => 'Insert column before',
'insert_column_after' => 'Insert column after',
'delete_column' => 'Delete column',
'table_cell' => 'Cell',
'table_row' => 'Row',
'table_column' => 'Column',
'cell_properties' => 'Cell properties',
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_type_header' => 'Header cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
'vertical_align' => 'Vertical align',
'border_width' => 'Border width',
'border_style' => 'Border style',
'border_color' => 'Border color',
'row_properties' => 'Row properties',
'row_properties_title' => 'Row Properties',
'cut_row' => 'Cut row',
'copy_row' => 'Copy row',
'paste_row_before' => 'Paste row before',
'paste_row_after' => 'Paste row after',
'row_type' => 'Row type',
'row_type_header' => 'Header',
'row_type_body' => 'Body',
'row_type_footer' => 'Footer',
'alignment' => 'Alignment',
'cut_column' => 'Cut column',
'copy_column' => 'Copy column',
'paste_column_before' => 'Paste column before',
'paste_column_after' => 'Paste column after',
'cell_padding' => 'Cell padding',
'cell_spacing' => 'Cell spacing',
'caption' => 'Caption',
'show_caption' => 'Show caption',
'constrain' => 'Constrain proportions',
'table_properties' => 'Настройки на таблицата',
'table_properties_title' => 'Настройки на таблицата',
'delete_table' => 'Изтрий таблицата',
'insert_row_before' => 'Вмъкни реда преди',
'insert_row_after' => 'Вмъкни реда след',
'delete_row' => 'Изтрий реда',
'insert_column_before' => 'Вмъкни колоната преди',
'insert_column_after' => 'Вмъкни колоната след',
'delete_column' => 'Изтрий колоната',
'table_cell' => 'Клетка',
'table_row' => 'Ред',
'table_column' => 'Колона',
'cell_properties' => 'Настройки на клетката',
'cell_properties_title' => 'Настройки на клетката',
'cell_type' => 'Тип на клетката',
'cell_type_cell' => 'Клетка',
'cell_scope' => 'Scope',
'cell_type_header' => 'Заглавна клетка',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Група от редове',
'table_column_group' => 'Група от колони',
'horizontal_align' => 'Хоризонтално разположение',
'vertical_align' => 'Вертикално разположение',
'border_width' => 'Дължината на рамката',
'border_style' => 'Стил на рамката',
'border_color' => 'Цвят на рамката',
'row_properties' => 'Свойства на реда',
'row_properties_title' => 'Свойства на реда',
'cut_row' => 'Изрежи реда',
'copy_row' => 'Копирай реда',
'paste_row_before' => 'Постави реда преди',
'paste_row_after' => 'Постави реда след',
'row_type' => 'Тип на реда',
'row_type_header' => 'Заглавка',
'row_type_body' => 'Тяло',
'row_type_footer' => 'Долна част',
'alignment' => 'Разположение',
'cut_column' => 'Изрежи колоната',
'copy_column' => 'Копирай колоната',
'paste_column_before' => 'Постави колоната преди',
'paste_column_after' => 'Постави колоната след',
'cell_padding' => 'Отстояние на клетката',
'cell_spacing' => 'Отстояние на клетката',
'caption' => 'Надпис',
'show_caption' => 'Покажи надпис',
'constrain' => 'Ограничи пропорциите',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
'alt_desc' => 'Alternative description',
'embed' => 'Embed',
'paste_embed' => 'Paste your embed code below:',
'source' => 'Източник',
'alt_desc' => 'Алтернативно описание',
'embed' => 'Вгради',
'paste_embed' => 'Постави кода за вмъкване отдолу:',
'url' => 'URL',
'text_to_display' => 'Text to display',
'title' => 'Title',
'open_link' => 'Open link in...',
'open_link_current' => 'Current window',
'open_link_new' => 'New window',
'insert_collapsible' => 'Insert collapsible block',
'collapsible_unwrap' => 'Unwrap',
'edit_label' => 'Edit label',
'toggle_open_closed' => 'Toggle open/closed',
'collapsible_edit' => 'Edit collapsible block',
'toggle_label' => 'Toggle label',
'text_to_display' => 'Текст за показване',
'title' => 'Заглавие',
'open_link' => 'Отваряне не връзката в...',
'open_link_current' => 'Текущ прозорец',
'open_link_new' => 'Нов прозорец',
'insert_collapsible' => 'Вмъкни сгъваем блок',
'collapsible_unwrap' => 'Разгъни',
'edit_label' => 'Редактирай етикета',
'toggle_open_closed' => 'Превключи отворено/затворено',
'collapsible_edit' => 'Редактирай сгъваем блок',
'toggle_label' => 'Превключи надписа',
// About view
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',
'about' => 'About the editor',
'about_title' => 'Относно визуалния редактор',
'editor_license' => 'Лиценз, авторски и сходни права на редактора',
'editor_tiny_license' => 'Този редактор е създаден с :tinyLink, който е предоставен с лиценз LGPL v2.1.',
'editor_tiny_license_link' => 'Авторското и сходните му права, както и лицензът на TinyMCE, могат да бъдат намерени тук.',
'save_continue' => 'Запази страницата и продължи',
'callouts_cycle' => '(Продължавай да натискаш, за да превключваш типовете)',
'link_selector' => 'Свържи със съдържанието',
'shortcuts' => 'Преки пътища',
'shortcut' => 'Пряк път',
'shortcuts_intro' => 'Следните клавишни комбинации са налични за редактора:',
'windows_linux' => '(Windows/Linux)',
'mac' => '(Mac)',
'description' => 'Description',
'description' => 'Описание',
];

View File

@@ -27,7 +27,7 @@ return [
'images' => 'Изображения',
'my_recent_drafts' => 'Моите скорошни драфтове',
'my_recently_viewed' => 'Моите скорошни преглеждания',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_most_viewed_favourites' => 'Моите най-преглеждани любими',
'my_favourites' => 'Моите фаворити',
'no_pages_viewed' => 'Не сте прегледали никакви страници',
'no_pages_recently_created' => 'Не са били създавани страници скоро',
@@ -36,7 +36,7 @@ return [
'export_html' => 'Прикачени уеб файлове',
'export_pdf' => 'PDF файл',
'export_text' => 'Обикновен текстов файл',
'export_md' => 'Markdown File',
'export_md' => 'Markdown файл',
// Permissions and restrictions
'permissions' => 'Права',
@@ -53,7 +53,7 @@ return [
'search_for_term' => 'Търси :term',
'search_more' => 'Още резултати',
'search_advanced' => 'Подробно търсене',
'search_terms' => 'Search Terms',
'search_terms' => 'Термини за търсене',
'search_content_type' => 'Тип на съдържание',
'search_exact_matches' => 'Точни съвпадения',
'search_tags' => 'Търсене на тагове',
@@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Настройки за достъп до рафта с книги',
'shelves_permissions_updated' => 'Настройките за достъп до рафта с книги е обновен',
'shelves_permissions_active' => 'Настройките за достъп до рафта с книги е активен',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_cascade_warning' => 'Привилегиите на рафтовете не се разпространяват автоматично към съдържаните в тях книги. Това е така, защото една книга може да съществува на няколко различни рафта. Въпреки това, привилегиите могат да бъдат копирани до книгите вътре чрез опцията отдолу.',
'shelves_copy_permissions_to_books' => 'Копирай настойките за достъп към книгите',
'shelves_copy_permissions' => 'Копирай настройките за достъп',
'shelves_copy_permissions_explain' => 'Това ще приложи настоящите настройки за достъп на този рафт с книги за всички книги, съдържащи се в него. Преди да активирате, уверете се, че всички промени в настройките за достъп на този рафт са запазени.',
@@ -143,8 +143,8 @@ return [
'books_sort_chapters_last' => 'Последна глава',
'books_sort_show_other' => 'Покажи други книги',
'books_sort_save' => 'Запази новата подредба',
'books_copy' => 'Copy Book',
'books_copy_success' => 'Book successfully copied',
'books_copy' => 'Копирай книгата',
'books_copy_success' => 'Книгата е копирана успешно',
// Chapters
'chapter' => 'Глава',
@@ -155,7 +155,7 @@ return [
'chapters_create' => 'Създай нова глава',
'chapters_delete' => 'Изтрий глава',
'chapters_delete_named' => 'Изтрий глава :chapterName',
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
'chapters_delete_explain' => 'Това ще изтрие главата \':chapterName\'. Всички страници в главата също ще бъдат изтрити.',
'chapters_delete_confirm' => 'Сигурни ли сте, че искате да изтриете тази глава?',
'chapters_edit' => 'Редактирай глава',
'chapters_edit_named' => 'Актуализирай глава :chapterName',
@@ -163,8 +163,8 @@ return [
'chapters_move' => 'Премести глава',
'chapters_move_named' => 'Премести глава :chapterName',
'chapter_move_success' => 'Главата беше преместена в :bookName',
'chapters_copy' => 'Copy Chapter',
'chapters_copy_success' => 'Chapter successfully copied',
'chapters_copy' => 'Копирай главата',
'chapters_copy_success' => 'Главата е копирана успешно',
'chapters_permissions' => 'Настойки за достъп на главата',
'chapters_empty' => 'Няма създадени страници в тази глава.',
'chapters_permissions_active' => 'Настройките за достъп до глава са активни',
@@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'Черновата е запазена в ',
'pages_edit_delete_draft' => 'Изтрий чернова',
'pages_edit_discard_draft' => 'Отхвърляне на черновата',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'Задайте регистър на промените',
'pages_edit_enter_changelog_desc' => 'Въведете кратко резюме на промените, които сте създали',
'pages_edit_enter_changelog' => 'Въведи регистър на промените',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Запазване на страницата',
'pages_title' => 'Заглавие на страницата',
'pages_name' => 'Име на страницата',
@@ -219,12 +229,13 @@ return [
'pages_revisions' => 'Ревизии на страницата',
'pages_revisions_named' => 'Ревизии на страницата :pageName',
'pages_revision_named' => 'Ревизия на страницата :pageName',
'pages_revision_restored_from' => 'Restored from #:id; :summary',
'pages_revision_restored_from' => 'Възстановено от #:id; :summary',
'pages_revisions_created_by' => 'Създадено от',
'pages_revisions_date' => 'Дата на ревизията',
'pages_revisions_number' => '№',
'pages_revisions_numbered' => 'Ревизия №:id',
'pages_revisions_numbered_changes' => 'Ревизия №:id Промени',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'История на промените',
'pages_revisions_changes' => 'Промени',
'pages_revisions_current' => 'Текуща версия',
@@ -238,7 +249,7 @@ return [
'pages_initial_name' => 'Нова страница',
'pages_editing_draft_notification' => 'В момента редактирате чернова, която беше последно обновена :timeDiff.',
'pages_draft_edited_notification' => 'Тази страница беше актуализирана от тогава. Препоръчително е да изтриете настоящата чернова.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_page_changed_since_creation' => 'Страницата е била обновена от създаването на черновата. Препоръчително е да изтриеш черновата или да се погрижиш да не презапишеш промени по страницата.',
'pages_draft_edit_active' => [
'start_a' => ':count потребителя започнаха да редактират настоящата страница',
'start_b' => ':userName в момента редактира тази страница',
@@ -262,16 +273,16 @@ return [
'tags_explain' => "Добавете няколко тага за да категоризирате по добре вашето съдържание. \n Може да добавите съдържание на таговете за по-подробна организация.",
'tags_add' => 'Добави друг таг',
'tags_remove' => 'Премахни този таг',
'tags_usages' => 'Total tag usages',
'tags_assigned_pages' => 'Assigned to Pages',
'tags_assigned_chapters' => 'Assigned to Chapters',
'tags_assigned_books' => 'Assigned to Books',
'tags_assigned_shelves' => 'Assigned to Shelves',
'tags_x_unique_values' => ':count unique values',
'tags_all_values' => 'All values',
'tags_view_tags' => 'View Tags',
'tags_view_existing_tags' => 'View existing tags',
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
'tags_usages' => 'Общо ползвания на таг',
'tags_assigned_pages' => 'Присвоен на страници',
'tags_assigned_chapters' => 'Присвоен на глави',
'tags_assigned_books' => 'Присвоен на книги',
'tags_assigned_shelves' => 'Присвоен на рафтове',
'tags_x_unique_values' => ':count уникални стойности',
'tags_all_values' => 'Всички стойности',
'tags_view_tags' => 'Виж тагове',
'tags_view_existing_tags' => 'Виж съществуващи тагове',
'tags_list_empty_hint' => 'Таговете могат да бъдат прилагани чрез страничната лента в редактора на страници или по време на редактирането на детайлите за книги, глави или рафтове.',
'attachments' => 'Прикачени файлове',
'attachments_explain' => 'Прикачете файлове или линкове, които да са видими на вашата страница. Същите ще бъдат видими във вашето странично поле.',
'attachments_explain_instant_save' => 'Промените тук се запазват веднага.',
@@ -288,7 +299,7 @@ return [
'attachments_link_url' => 'Линк към файла',
'attachments_link_url_hint' => 'Url на сайт или файл',
'attach' => 'Прикачване',
'attachments_insert_link' => 'Add Attachment Link to Page',
'attachments_insert_link' => 'Добави линк на прикачен файл към страница',
'attachments_edit_file' => 'Редактирай файл',
'attachments_edit_file_name' => 'Име на файл',
'attachments_edit_drop_upload' => 'Поставете файл или цъкнете тук за да прикачите и обновите',
@@ -338,10 +349,10 @@ return [
'revision_cannot_delete_latest' => 'Не може да изтриете последната версия.',
// Copy view
'copy_consider' => 'Please consider the below when copying content.',
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
'copy_consider_owner' => 'You will become the owner of all copied content.',
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
'copy_consider_attachments' => 'Page attachments will not be copied.',
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
'copy_consider' => 'Моля, имай предвид долното при копиране на съдържание.',
'copy_consider_permissions' => 'Специфичните настройки на привилегиите няма да бъдат копирани.',
'copy_consider_owner' => 'Ти ще станеш собственикът на цялото копирано съдържание.',
'copy_consider_images' => 'Файловете на изображенията в страницата няма да бъдат дубликирани и оригиналните изображения ще запазят връзката си със страницата, на която са били качени първоначално.',
'copy_consider_attachments' => 'Прикачените към страницата обекти няма да бъдат копирани.',
'copy_consider_access' => 'Смяна на местоположението, собственика или привилегиите може да направи това съдържание достъпно за тези, които не са го виждали преди.',
];

View File

@@ -11,10 +11,10 @@ return [
// Auth
'error_user_exists_different_creds' => 'Потребител с емайл :email вече съществува но с други данни.',
'email_already_confirmed' => 'Емейлът вече беше потвърден. Моля опитрайте да влезете.',
'email_confirmation_invalid' => 'Този код за достъп не е валиден или вече е бил използван, Моля опитрайте се да се регистрирате отново.',
'email_confirmation_invalid' => 'Този код за достъп не е валиден или вече е бил използван, Моля опитай да се регистрираш отново.',
'email_confirmation_expired' => 'Кодът за потвърждение изтече, нов емейл за потвърждение беше изпратен.',
'email_confirmation_awaiting' => 'Емайл адреса, който използвате трябва да се потвърди',
'ldap_fail_anonymous' => 'LDAP протокола прекъсна, използвайки анонимни настройки',
'ldap_fail_anonymous' => 'LDAP достъпът е неуспешен с анонимни настройки',
'ldap_fail_authed' => 'Опита за достъп чрез LDAP с използваната парола не беше успешен',
'ldap_extension_not_installed' => 'LDAP PHP не беше инсталирана',
'ldap_cannot_connect' => 'Не може да се свържете с Ldap сървъра, първоначалната връзка се разпадна',
@@ -22,62 +22,62 @@ return [
'saml_user_not_registered' => 'Потребителят :name не е регистриран и автоматичната регистрация не е достъпна',
'saml_no_email_address' => 'Не успяхме да намерим емейл адрес, за този потребител, от информацията предоставена от външната система',
'saml_invalid_response_id' => 'Заявката от външната система не е разпознат от процеса започнат от това приложение. Връщането назад след влизане може да породи този проблем.',
'saml_fail_authed' => 'Влизането чрез :system не беше успешно, системата не успя да оторизира потребителя',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'saml_fail_authed' => 'Влизането чрез :system не беше успешно, системата не успя да удостовери потребителя',
'oidc_already_logged_in' => 'Вече си вписан',
'oidc_user_not_registered' => 'Потребителят :name не е регистриран, а автоматичната регистрация е изключена',
'oidc_no_email_address' => 'Не можах да намеря имейл адрес за този потребител в данните, предоставени от външната удостоверителна система',
'oidc_fail_authed' => 'Вписването чрез :system не беше успешно, тъй като системата не предостави успешна оторизация',
'social_no_action_defined' => 'Действието не беше дефинирано',
'social_login_bad_response' => "Възникна грешка по време на :socialAccount login: \n:error",
'social_account_in_use' => 'Този :socialAccount вече е използван. Опитайте се да влезете чрез опцията за :socialAccount.',
'social_account_email_in_use' => 'Този емейл адрес вече е бил използван. Ако вече имате профил, може да го свържете чрез :socialAccount от вашия профил.',
'social_account_existing' => 'Този :socialAccount вече в свързан с вашия профил.',
'social_account_already_used_existing' => 'Този :socialAccount вече се използва от друг потребител.',
'social_account_not_used' => 'Този :socialAccount не е свързан с профил. Моля свържете го с вашия профил. ',
'social_account_register_instructions' => 'Ако все още нямате профил, може да се регистрирате чрез :socialAccount опцията.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
'social_account_not_used' => 'Социалният профил :socialAccount не е свързан с потребител. Моля, свържи го в настройките на профила си. ',
'social_account_register_instructions' => 'Ако все още нямаш профил, може да се регистрираш чрез опцията :socialAccount.',
'social_driver_not_found' => 'Кодът за връзка със социалната мрежа не съществува',
'social_driver_not_configured' => 'Социалните настройки на твоя :socialAccount не са конфигурирани правилно.',
'invite_token_expired' => 'Твоята покана е изтекла. Вместо това може да пробваш да възстановиш паролата на профила си.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Cannot get image from :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'uploaded' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'image_upload_error' => 'An error occurred uploading the image',
'image_upload_type_error' => 'The image type being uploaded is invalid',
'file_upload_timeout' => 'The file upload has timed out.',
'path_not_writable' => 'Не може да се качи файл в :filePath. Увери се на сървъра, че в пътя може да се записва.',
'cannot_get_image_from_url' => 'Не мога да взема съобщението от :url',
'cannot_create_thumbs' => 'Сървърът не може да създаде малки изображения. Моля, увери се, че разширението GD PHP е инсталирано.',
'server_upload_limit' => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',
'uploaded' => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',
'image_upload_error' => 'Възникна грешка при качването на изображението',
'image_upload_type_error' => 'Типът на качваното изображение е невалиден',
'file_upload_timeout' => 'Качването на файла изтече.',
// Attachments
'attachment_not_found' => 'Attachment not found',
'attachment_not_found' => 'Прикачения файл не е намерен',
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
'page_draft_autosave_fail' => 'Неуспешно запазване на черновата. Увери се, че имаш свързаност с интернет преди да запазиш страницата',
'page_custom_home_deletion' => 'Не мога да изтрия страницата, докато е настроена като начална',
// Entities
'entity_not_found' => 'Entity not found',
'bookshelf_not_found' => 'Bookshelf not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
'selected_book_not_found' => 'The selected book was not found',
'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
'guests_cannot_save_drafts' => 'Guests cannot save drafts',
'entity_not_found' => 'Обектът не е намерен',
'bookshelf_not_found' => 'Рафтът не е намерен',
'book_not_found' => 'Книгата не е намерена',
'page_not_found' => 'Страницата не е намерена',
'chapter_not_found' => 'Главата не е намерена',
'selected_book_not_found' => 'Избраната книга не е намерена',
'selected_book_chapter_not_found' => 'Избраната книга или глава не е намерена',
'guests_cannot_save_drafts' => 'Гостите не могат да запазват чернови',
// Users
'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
'users_cannot_delete_guest' => 'You cannot delete the guest user',
'users_cannot_delete_only_admin' => 'Не можеш да изтриеш единствения администратор',
'users_cannot_delete_guest' => 'Не можеш да изтриеш потребителя на госта',
// Roles
'role_cannot_be_edited' => 'This role cannot be edited',
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',
'role_cannot_be_edited' => 'Ролята не може да бъде редактирана',
'role_system_cannot_be_deleted' => 'Тази роля е системна и не може да бъде изтрита',
'role_registration_default_cannot_delete' => 'Тази роля не може да бъде изтрита, докато е настроена по подразбиране за нови регистрации',
'role_cannot_remove_only_admin' => 'Този потребител е единственият с присвоена администраторска роля. Приложи администраторската роля на друг потребител, преди да я премахнеш от тук.',
// Comments
'comment_list' => 'An error occurred while fetching the comments.',
'comment_list' => 'Настъпи грешка при зареждането на коментарите.',
'cannot_add_comment_to_draft' => 'Не може да добавяте коментари към чернова.',
'comment_add' => 'Възникна грешка при актуализиране/добавяне на коментар.',
'comment_delete' => 'Възникна грешка при изтриването на коментара.',
@@ -87,9 +87,9 @@ return [
'404_page_not_found' => 'Страницата не е намерена',
'sorry_page_not_found' => 'Страницата, която търсите не може да бъде намерена.',
'sorry_page_not_found_permission_warning' => 'Ако смятате, че тази страница съществува, най-вероятно нямате право да я преглеждате.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'image_not_found' => 'Изображението не е намерено',
'image_not_found_subtitle' => 'Съжалявам, файлът на изображението, което търсиш, не може да бъде намерен.',
'image_not_found_details' => 'Ако си очаквал/а това изображение да същестува, може да е било изтрито.',
'return_home' => 'Назад към Начало',
'error_occurred' => 'Възникна грешка',
'app_down' => ':appName не е достъпно в момента',

View File

@@ -7,9 +7,9 @@
return [
'password' => 'Паролите трябва да имат поне 8 символа и да съвпадат с потвърждението.',
'user' => "Не можем да намерим потребител с този имейл адрес.",
'token' => 'Кодът за зануляване на паролата е невалиден за този емейл адрес.',
'sent' => 'Пратихме връзка за нулиране на паролата до имейла ви!',
'reset' => 'Вашата парола е нулирана!',
'user' => "Не може да се намери потребител с този имейл адрес.",
'token' => 'Кодът за възстановяване на паролата е невалиден за този емейл адрес.',
'sent' => 'На имейла ти е изпратена връзка за възстановяване на паролата ти!',
'reset' => 'Парола ти е възстановена!',
];

View File

@@ -10,6 +10,8 @@ return [
'settings' => 'Настройки',
'settings_save' => 'Запази настройките',
'settings_save_success' => 'Настройките са записани',
'system_version' => 'System Version',
'categories' => 'Categories',
// App Settings
'app_customization' => 'Персонализиране',
@@ -25,8 +27,8 @@ return [
'app_secure_images' => 'По-висока сигурност при качване на изображения',
'app_secure_images_toggle' => 'Активиране на по-висока сигурност при качване на изображения',
'app_secure_images_desc' => 'С цел производителност, всички изображения са публични. Тази настройка добавя случаен, труден за отгатване низ от символи пред линка на изображението. Подсигурете, че индексите на директорията не са включени за да предотвратите лесен достъп.',
'app_editor' => 'Редактор на страница',
'app_editor_desc' => 'Изберете кой редактор да се използва от всички потребители за да редактират страници.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Персонализирано съдържание на HTML шапката',
'app_custom_html_desc' => 'Всяко съдържание, добавено тук, ще бъде поставено в долната част на секцията <head> на всяка страница. Това е удобно за преобладаващи стилове или добавяне на код за анализ.',
'app_custom_html_disabled_notice' => 'Съдържанието на персонализираната HTML шапка е деактивирано на страницата с настройки, за да се гарантира, че евентуални лоши промени могат да бъдат върнати.',
@@ -34,52 +36,52 @@ return [
'app_logo_desc' => 'Това изображение трябва да е с 43px височина. <br> Големите изображения ще бъдат намалени.',
'app_primary_color' => 'Основен цвят на приложението',
'app_primary_color_desc' => 'Изберете основния цвят на приложението, включително на банера, бутоните и линковете.',
'app_homepage' => 'Application Homepage',
'app_homepage' => 'Начлна страница на приложението',
'app_homepage_desc' => 'Изберете начална страница, която ще замени изгледа по подразбиране. Дефинираните права на страницата, която е избрана ще бъдат игнорирани.',
'app_homepage_select' => 'Избери страница',
'app_footer_links' => 'Футър линкове',
'app_footer_links_desc' => 'Добави линк в съдържанието на футъра. Добавените линкове ще се показват долу в повечето страници, включително и в страниците, в които логването не е задължително. Можете да използвате заместител "trans::<key>", за да използвате дума дефинирана от системата. Пример: Използването на "trans::common.privacy_policy" ще покаже "Лични данни" или на "trans::common.terms_of_service" ще покаже "Общи условия".',
'app_footer_links_label' => 'Link Label',
'app_footer_links_label' => 'Надпис на връзката',
'app_footer_links_url' => 'Линк URL',
'app_footer_links_add' => 'Добави футър линк',
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
'app_disable_comments' => 'Изключи коментарите',
'app_disable_comments_toggle' => 'Изключи коментарите',
'app_disable_comments_desc' => 'Изключва коментарите във всички на страници на приложението. <br> Съществуващите коментари няма да се показват.',
// Color settings
'content_colors' => 'Content Colors',
'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
'bookshelf_color' => 'Shelf Color',
'book_color' => 'Book Color',
'chapter_color' => 'Chapter Color',
'page_color' => 'Page Color',
'page_draft_color' => 'Page Draft Color',
'content_colors' => 'Цвят на съдържанието',
'content_colors_desc' => 'Настройва цветовете за всички елементи на йерархията за организацията на страницата. Избор на цвят с яркост, близка до цветовете по подразбиране, се препоръчва за четимостта.',
'bookshelf_color' => 'Цвят на рафта',
'book_color' => 'Цвят на книгата',
'chapter_color' => 'Цвят на главата',
'page_color' => 'Цвят на страницата',
'page_draft_color' => 'Цвят на черновата за страница',
// Registration Settings
'reg_settings' => 'Registration',
'reg_enable' => 'Enable Registration',
'reg_enable_toggle' => 'Enable registration',
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
'reg_default_role' => 'Default user role after registration',
'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
'reg_email_confirmation' => 'Email Confirmation',
'reg_email_confirmation_toggle' => 'Require email confirmation',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
'reg_confirm_restrict_domain' => 'Domain Restriction',
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
'reg_settings' => 'Регистрация',
'reg_enable' => 'Включи регистрацията',
'reg_enable_toggle' => 'Включи регистрацията',
'reg_enable_desc' => 'Когато регистрацията е включена, потребителите ще могат да се регистрират като потребители на приложението. След регистрация на тях им се дава роля по подразбиране.',
'reg_default_role' => 'Роля по подразбиране след регистрация',
'reg_enable_external_warning' => 'Опцията отгоре се игнорира при активно външно LDAP или SAML удостоверяване. Ако удостоверяването от външната система е успешно, автоматично ще се създават потребителски профили за несъществуващи членове.',
'reg_email_confirmation' => 'Имейл потвърждение',
'reg_email_confirmation_toggle' => 'Изисквай имейл потвърждение',
'reg_confirm_email_desc' => 'Ако се използват ограничения за домейна, ще се изисква имейл потвърждение и тази настройка ще бъде игнорирана.',
'reg_confirm_restrict_domain' => 'Ограничения за домейна',
'reg_confirm_restrict_domain_desc' => 'Въведи разделен със запетаи списък от имейл домейни, до които да бъде ограничена регистрацията. На потребителите ще им бъде изпратен имейл, за да потвърдят адреса, преди да могат да използват приложението. <br> Имай предвид, че потребителите ще могат да сменят имейл адресите си след успешна регистрация.',
'reg_confirm_restrict_domain_placeholder' => 'Няма наложени ограничения',
// Maintenance settings
'maint' => 'Maintenance',
'maint_image_cleanup' => 'Cleanup Images',
'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',
'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
'maint_image_cleanup_run' => 'Run Cleanup',
'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
'maint_send_test_email' => 'Send a Test Email',
'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
'maint' => 'Поддръжка',
'maint_image_cleanup' => 'Разчисти изображения',
'maint_image_cleanup_desc' => 'Сканира съдържанието на страници и ревизиите, за да провери кои изображения и рисунки се използват и кои се повтарят. Увери се, че имаш пълни резервни копия на базата данни и на изображенията, преди да пуснеш това.',
'maint_delete_images_only_in_revisions' => 'Също изтрий изображенията, които съществуват само в стари ревизии на страниците',
'maint_image_cleanup_run' => 'Пусни разчистване',
'maint_image_cleanup_warning' => 'Намерени са :count потенциално неизползвани изображения. Сигурен/на ли си, че искаш да изтриеш тези изображения?',
'maint_image_cleanup_success' => 'Намерени и изтрити са :count потенциално неизползвани изображения!',
'maint_image_cleanup_nothing_found' => 'Не са намерени неизползвани изображения и нищо не е изтрито!',
'maint_send_test_email' => 'Изпрати тестови имейл',
'maint_send_test_email_desc' => 'Това изпраща тестови имейл на имейл адреса, посочен в профила ти.',
'maint_send_test_email_run' => 'Изпрати тестов имейл',
'maint_send_test_email_success' => 'Имейл изпратен на :address',
'maint_send_test_email_mail_subject' => 'Тестов Имейл',
@@ -90,36 +92,36 @@ return [
// Recycle Bin
'recycle_bin' => 'Кошче',
'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'recycle_bin_desc' => 'Тук може да възстановиш изтрити обекти или да ги премахнеш завинаги от системата. Този списък не е филтриран, за разлика от подобни списъци с активност в системата, където са приложени списъци за привилегии.',
'recycle_bin_deleted_item' => 'Изтрит предмет',
'recycle_bin_deleted_parent' => 'Parent',
'recycle_bin_deleted_parent' => 'Родител',
'recycle_bin_deleted_by' => 'Изтрит от',
'recycle_bin_deleted_at' => 'Час на изтриване',
'recycle_bin_permanently_delete' => 'Permanently Delete',
'recycle_bin_restore' => 'Restore',
'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
'recycle_bin_empty' => 'Empty Recycle Bin',
'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',
'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?',
'recycle_bin_destroy_list' => 'Items to be Destroyed',
'recycle_bin_restore_list' => 'Items to be Restored',
'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',
'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',
'recycle_bin_restore_parent' => 'Restore Parent',
'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
'recycle_bin_permanently_delete' => 'Изтрий завинаги',
'recycle_bin_restore' => 'Възстанови',
'recycle_bin_contents_empty' => 'Кошчето е празно',
'recycle_bin_empty' => 'Изпразни кочшето',
'recycle_bin_empty_confirm' => 'Това ще унищожи завинаги всички обекти в кошчето, включително съдържанието във всеки обект. Сигурен/на ли си, че искаш да изпразниш кошчето?',
'recycle_bin_destroy_confirm' => 'Това действие завинаги ще изтрие от системата този обект, както и всички негови поделементи, и няма да можеш да го възстановиш. Сигурен/на ли си, че искаш да изтриеш този обект завинаги?',
'recycle_bin_destroy_list' => 'Обекти за унищожение',
'recycle_bin_restore_list' => 'Обекти за възстановяване',
'recycle_bin_restore_confirm' => 'Това действие ще възстанови изтрития обект, както и всички негови поделементи, в оригиналното им местоположение. Ако оригиналното им местоположение също е изтрито и сега се намира в кошчето, то също ще трябва да бъде възстановено.',
'recycle_bin_restore_deleted_parent' => 'Родителският елемент на този обект също е бил изтрит. Тези ще останат изтрити, докато родителят също бъде възстановен.',
'recycle_bin_restore_parent' => 'Възстанови родителския елемент',
'recycle_bin_destroy_notification' => 'Изтрити общо :count обекта от кошчето.',
'recycle_bin_restore_notification' => 'Възстановени общо :count обекта от кошчето.',
// Audit Log
'audit' => 'Audit Log',
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'audit_event_filter' => 'Event Filter',
'audit' => 'Ревизорен журнал',
'audit_desc' => 'Ревизорният журнал показва списък с всички дейности, следенив системата. Това е нефилтриран списък, за разлика от подобни списъци с дейности в системата, където са приложени филтри за привилегии.',
'audit_event_filter' => 'Филтър на събитията',
'audit_event_filter_no_filter' => 'Без филтър',
'audit_deleted_item' => 'Изтрит предмет',
'audit_deleted_item_name' => 'Име: :name',
'audit_table_user' => 'Потребител',
'audit_table_event' => 'Събитие',
'audit_table_related' => 'Related Item or Detail',
'audit_table_ip' => 'IP Address',
'audit_table_related' => 'Свързан обект или детайл',
'audit_table_ip' => 'IP адрес',
'audit_table_date' => 'Дата на активност',
'audit_date_from' => 'Време от',
'audit_date_to' => 'Време до',
@@ -139,7 +141,7 @@ return [
'role_details' => 'Детайли на роля',
'role_name' => 'Име на ролята',
'role_desc' => 'Кратко описание на ролята',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_mfa_enforced' => 'Изисква многофакторно удостоверяване',
'role_external_auth_id' => 'Външни ауторизиращи ID-a',
'role_system' => 'Настойки за достъп на системата',
'role_manage_users' => 'Управление на потребители',
@@ -149,14 +151,15 @@ return [
'role_manage_page_templates' => 'Управление на шаблони на страници',
'role_access_api' => 'Достъп до API на системата',
'role_manage_settings' => 'Управление на настройките на приложението',
'role_export_content' => 'Export content',
'role_export_content' => 'Експортирай съдържанието',
'role_editor_change' => 'Change page editor',
'role_asset' => 'Настройки за достъп до активи',
'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',
'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',
'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.',
'role_all' => 'Всички',
'role_own' => 'Собствени',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_controlled_by_asset' => 'Контролирани от актива, към който са качени',
'role_save' => 'Запази ролята',
'role_update_success' => 'Ролята беше успешно актуализирана',
'role_users' => 'Потребители в тази роля',
@@ -169,94 +172,94 @@ return [
'users_search' => 'Търси Потребители',
'users_latest_activity' => 'Последна активност',
'users_details' => 'Потребителски детайли',
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
'users_role' => 'User Roles',
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
'users_password' => 'User Password',
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
'users_send_invite_option' => 'Send user invite email',
'users_external_auth_id' => 'External Authentication ID',
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
'users_password_warning' => 'Only fill the below if you would like to change your password.',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_details_desc' => 'Настрой име и имейл адрес за този потребител. Имейл адресът ще се използва за вписване в приложението.',
'users_details_desc_no_email' => 'Настрой име за този потребител, за да могат другите да го разпознават.',
'users_role' => 'Потребителски роли',
'users_role_desc' => 'Настрой ролите, които ще бъдат присвоени на този потребител. Ако му бъдат присвоени няколко роли, привилегиите от тях ще се насложат и потребителят ще получи всички привилегии на зададените роли.',
'users_password' => 'Потребителска парола',
'users_password_desc' => 'Настрой парола за вписване в приложението. Тя трябва да бъде дълга поне 8 знака.',
'users_send_invite_text' => 'Можеш да изпратиш на потребителя покана по имейл, след което той ще може да настрои своя собствена парола. В противен случай, ти също можеш да настроиш паролата му.',
'users_send_invite_option' => 'Изпрати на потребителя имейл покана',
'users_external_auth_id' => 'Външен номер за удостоверяване',
'users_external_auth_id_desc' => 'Това е номерът, използван за сверяване на потребители при комуникация с конфигурираната външна система за удостоверяване.',
'users_password_warning' => 'Попълни отдолу само ако желаеш да смениш паролата си.',
'users_system_public' => 'Този потребител представлява всеки гост, който посещава това приложение. Потребителят не може да се използва за вписване, а вместо това се присвоява автоматично.',
'users_delete' => 'Изтрий потребител',
'users_delete_named' => 'Изтрий потребителя :userName',
'users_delete_warning' => 'Това изцяло ще изтрие този потребител с името \':userName\' от системата.',
'users_delete_confirm' => 'Сигурни ли сте, че искате да изтриете този потребител?',
'users_migrate_ownership' => 'Мигрирайте собствеността на сайта',
'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
'users_migrate_ownership_desc' => 'Тук избери потребител, ако желаеш друг да стане собственик на всички обекти, които към момента са притежавани от този потребител.',
'users_none_selected' => 'Няма избрани потребители',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_avatar' => 'User Avatar',
'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',
'users_preferred_language' => 'Preferred Language',
'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
'users_social_accounts' => 'Social Accounts',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Connect Account',
'users_social_disconnect' => 'Disconnect Account',
'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
'users_api_tokens' => 'API Tokens',
'users_api_tokens_none' => 'No API tokens have been created for this user',
'users_api_tokens_create' => 'Create Token',
'users_api_tokens_expires' => 'Expires',
'users_api_tokens_docs' => 'API Documentation',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users_edit' => 'Редактирай потребител',
'users_edit_profile' => 'Редактирай профил',
'users_avatar' => 'Потребителски аватар',
'users_avatar_desc' => 'Избери изображение, което да представлява този потребител. То трябва да бъде квадрат с размер приблизително 256 пиксела.',
'users_preferred_language' => 'Предпочитан език',
'users_preferred_language_desc' => 'Тази настройка ще промени езика за потребителския интерфейс на приложението. Това няма да се отрази на създаденото от потребителите съдържание.',
'users_social_accounts' => 'Социални профили',
'users_social_accounts_info' => 'Тук можеш да свържеш другите си профили за по-бързо и лесно вписване. Отвързването на профил тук няма да анулира предишно удостоверен достъп. Вместо това, спри достъпа от настройките на профила си в свързаната социална мрежа.',
'users_social_connect' => 'Свържи профил',
'users_social_disconnect' => 'Отвържи профил',
'users_social_connected' => 'Профилът :socialAccount беше успешно свързан с профила ти.',
'users_social_disconnected' => 'Профилът :socialAccount беше успешно отвързан от профила ти.',
'users_api_tokens' => 'API маркери',
'users_api_tokens_none' => 'Няма създадени API маркери за този потребител',
'users_api_tokens_create' => 'Създай маркер',
'users_api_tokens_expires' => 'Изтича на',
'users_api_tokens_docs' => 'Документация на API',
'users_mfa' => 'Многофакторно удостоверяване',
'users_mfa_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.',
'users_mfa_x_methods' => ':count метод е настроен|:count методи са настроени',
'users_mfa_configure' => 'Конфигурирай методи',
// API Tokens
'user_api_token_create' => 'Create API Token',
'user_api_token_name' => 'Name',
'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
'user_api_token_expiry' => 'Expiry Date',
'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
'user_api_token_create_success' => 'API token successfully created',
'user_api_token_update_success' => 'API token successfully updated',
'user_api_token' => 'API Token',
'user_api_token_id' => 'Token ID',
'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
'user_api_token_secret' => 'Token Secret',
'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
'user_api_token_created' => 'Token created :timeAgo',
'user_api_token_updated' => 'Token updated :timeAgo',
'user_api_token_delete' => 'Delete Token',
'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
'user_api_token_delete_success' => 'API token successfully deleted',
'user_api_token_create' => 'Създай API маркер',
'user_api_token_name' => 'Име',
'user_api_token_name_desc' => 'Дай на маркера си четимо име като бъдещо напомняне за предназначението му.',
'user_api_token_expiry' => 'Дата на изтичане',
'user_api_token_expiry_desc' => 'Настрой дата на изтичане на този маркер. След тази дата, заявки направени с този маркер вече няма да работят. Ако оставиш това поле празно, маркерът ще изтече след 100 години.',
'user_api_token_create_secret_message' => 'Веднага след създаването на този маркер ще се генерират и покажат "Номер на маркер" и "Тайна на маркер". Тайната ще бъде показана само веднъж, така че се увери, че си я копирал на сигурно място, преди да продължиш.',
'user_api_token_create_success' => 'API маркерът е създаден успешно',
'user_api_token_update_success' => 'API маркерът е редактиран успешно',
'user_api_token' => 'API маркер',
'user_api_token_id' => 'Номер на маркер',
'user_api_token_id_desc' => 'Това е нередактируем, системно генериран идентификатор за този маркер, който ще бъде необходимо да бъде предоставян в API заявките.',
'user_api_token_secret' => 'Тайна на маркер',
'user_api_token_secret_desc' => 'Това е системно генерирана тайна за този маркер, която ще бъде необходимо да бъде предоставяна в API заявки. Тайната ще бъде показана само веднъж, така че се увери, че си я копирал на сигурно място.',
'user_api_token_created' => 'Маркерът е създаден :timeAgo',
'user_api_token_updated' => 'Маркерът е редактиран :timeAgo',
'user_api_token_delete' => 'Изтрий маркер',
'user_api_token_delete_warning' => 'Това ще изтрие напълно API маркерът с име \':tokenName\' от системата.',
'user_api_token_delete_confirm' => 'Сигурен/на ли си, че искаш да изтриеш този API маркер?',
'user_api_token_delete_success' => 'API маркерът е изтрит успешно',
// Webhooks
'webhooks' => 'Webhooks',
'webhooks_create' => 'Create New Webhook',
'webhooks_none_created' => 'No webhooks have yet been created.',
'webhooks_edit' => 'Edit Webhook',
'webhooks_save' => 'Save Webhook',
'webhooks_details' => 'Webhook Details',
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
'webhooks_events' => 'Webhook Events',
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
'webhooks_events_all' => 'All system events',
'webhooks_name' => 'Webhook Name',
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
'webhooks_endpoint' => 'Webhook Endpoint',
'webhooks_active' => 'Webhook Active',
'webhook_events_table_header' => 'Events',
'webhooks_delete' => 'Delete Webhook',
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
'webhooks_format_example' => 'Webhook Format Example',
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
'webhooks_status' => 'Webhook Status',
'webhooks_last_called' => 'Last Called:',
'webhooks_last_errored' => 'Last Errored:',
'webhooks_last_error_message' => 'Last Error Message:',
'webhooks' => 'Уебкука',
'webhooks_create' => 'Създай нова уебкука',
'webhooks_none_created' => 'Няма създадени уебкуки.',
'webhooks_edit' => 'Редактирай уебкука',
'webhooks_save' => 'Запази уебкука',
'webhooks_details' => 'Подробности за уебкука',
'webhooks_details_desc' => 'Въведи име и POST крайна точка като местоположение, на което уебкуката да изпраща данни.',
'webhooks_events' => 'Събития на уебкуката',
'webhooks_events_desc' => 'Избери всички събития, които ще задействат съответната уебкука.',
'webhooks_events_warning' => 'Имай предвид, че тези събития ще се задействат за всички избрани събития, дори при приложени специфични привилегии. Увери се, че употребата на тази уебкука няма да разкрие чувствително съдържание.',
'webhooks_events_all' => 'Всички системни събития',
'webhooks_name' => 'Име на уебкука',
'webhooks_timeout' => 'Време за изтичане на заявката не уебкуката (в секунди)',
'webhooks_endpoint' => 'Крайна точка на уебкуката',
'webhooks_active' => 'Уебкуката е активна',
'webhook_events_table_header' => 'Събития',
'webhooks_delete' => 'Изтрий уебкуката',
'webhooks_delete_warning' => 'Това ще изтрие изцяло уебкуката с име \':webhookName\' от системата.',
'webhooks_delete_confirm' => 'Сигурен/на ли си, че искаш да изтриеш тази уебкука?',
'webhooks_format_example' => 'Примерен формат на уебкука',
'webhooks_format_example_desc' => 'Данните на уебкуката се изпращат като POST заявки към конфигурираната крайна точка като JSON, следвайки формата отдолу. Свойствата "related_item" и "url" са по желание и зависят от типа на задействаното събитие.',
'webhooks_status' => 'Статус на уебкука',
'webhooks_last_called' => 'Последно извикан на:',
'webhooks_last_errored' => 'Последна грешка на:',
'webhooks_last_error_message' => 'Последно съобщение за грешка:',
//! If editing translations files directly please ignore this in all
@@ -275,6 +278,8 @@ return [
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti keel',
'eu' => 'Euskara',
'fa' => 'فارسی',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@@ -15,13 +15,13 @@ return [
'alpha_dash' => ':attribute може да съдържа само букви, числа, тире и долна черта.',
'alpha_num' => ':attribute може да съдържа само букви и числа.',
'array' => ':attribute трябва да е масив (array).',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'Предоставеният код не е валиден или вече е бил използван.',
'before' => ':attribute трябва да е дата след :date.',
'between' => [
'numeric' => ':attribute трябва да е между :min и :max.',
'file' => ':attribute трябва да е между :min и :max килобайта.',
'string' => 'Дължината на :attribute трябва да бъде между :min и :max символа.',
'array' => 'The :attribute must have between :min and :max items.',
'array' => 'Атрибутът :attribute трябва да има между :min и :max елемента.',
],
'boolean' => 'Полето :attribute трябва да съдържа булева стойност (true или false).',
'confirmed' => 'Потвърждението на :attribute не съвпада.',
@@ -32,19 +32,19 @@ return [
'digits_between' => ':attribute трябва да бъде с дължина между :min и :max цифри.',
'email' => ':attribute трябва да бъде валиден имейл адрес.',
'ends_with' => ':attribute трябва да свършва с един от следните символи: :values',
'file' => 'The :attribute must be provided as a valid file.',
'file' => 'Атрибутът :attribute трябва да бъде предоставен като валиден файл.',
'filled' => 'Полето :attribute е задължителен.',
'gt' => [
'numeric' => ':attribute трябва да бъде по-голям от :value.',
'file' => 'Големината на :attribute трябва да бъде по-голямо от :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-голямо от :value символа.',
'array' => 'The :attribute must have more than :value items.',
'array' => 'Атрибутът :attribute трябва да има повече от :value елемента.',
],
'gte' => [
'numeric' => 'The :attribute must be greater than or equal :value.',
'numeric' => 'Атрибутът :attribute трябва бъде равен на или по-голям от :value.',
'file' => 'Големината на :attribute трябва да бъде по-голямо или равно на :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-голямо или равно на :value символа.',
'array' => 'The :attribute must have :value items or more.',
'array' => 'Атрибутът :attribute трябва да има поне :value елемента или повече.',
],
'exists' => 'Избраният :attribute е невалиден.',
'image' => ':attribute трябва да e изображение.',
@@ -59,56 +59,56 @@ return [
'numeric' => ':attribute трябва да бъде по-малко от :value.',
'file' => 'Големината на :attribute трябва да бъде по-малко от :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-малко от :value символа.',
'array' => 'The :attribute must have less than :value items.',
'array' => 'Атрибутът :attribute трябва да има по-малко от :value елемента.',
],
'lte' => [
'numeric' => ':attribute трябва да бъде по-малко или равно на :value.',
'file' => 'Големината на :attribute трябва да бъде по-малко или равно на :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-малко или равно на :value символа.',
'array' => 'The :attribute must not have more than :value items.',
'array' => 'Атрибутът :attribute не трябва да има повече от :value елемента.',
],
'max' => [
'numeric' => ':attribute не трябва да бъде по-голям от :max.',
'file' => 'Големината на :attribute не може да бъде по-голямо от :value килобайта.',
'string' => 'Дължината на :attribute не може да бъде по-голямо от :value символа.',
'array' => 'The :attribute may not have more than :max items.',
'array' => 'Атрибутът :attribute не може да има повече от :max елемента.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimes' => 'Атрибутът :attribute трябва да бъде файл от тип: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
'numeric' => 'Атрибутът :attribute трябва да бъде поне :min.',
'file' => 'Атрибутът :attribute трябва да бъде поне :min килобайта.',
'string' => 'Атрибутът :attribute трябва да бъде съдържа поне :min символа.',
'array' => 'Атрибутът :attribute трябва да има поне :min елемента.',
],
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'safe_url' => 'The provided link may not be safe.',
'not_in' => 'Избраният :attribute не е валиден.',
'not_regex' => 'Форматът на :attribute не е валиден.',
'numeric' => 'Атрибутът :attribute трябва да бъде число.',
'regex' => 'Форматът на :attribute не е валиден.',
'required' => 'Полето :attribute е задължително.',
'required_if' => 'Полето :attribute е задължително, когато :other е :value.',
'required_with' => 'Полето :attribute е задължително, когато :values е налично.',
'required_with_all' => 'Полето :attribute е задължително, когато :values са налични.',
'required_without' => 'Полето :attribute е задължително, когато :values не е налично.',
'required_without_all' => 'Полето :attribute е задължително, когато никоя стойност от :values не е налична.',
'same' => 'Атрибутът :attribute и :other трябва да си съвпадат.',
'safe_url' => 'Предоставеният линк може да не е сигурен.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
'numeric' => 'Атрибутът :attribute трябва да бъде :size.',
'file' => 'Атрибутът :attribute трябва да бъде :size килобайта.',
'string' => 'Атрибутът :attribute трябва да бъде с дължина :size знака.',
'array' => 'Атрибутът :attribute трябва да съдържа :size елемента.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'totp' => 'The provided code is not valid or has expired.',
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
'string' => 'Атрибутът :attribute трябва да бъде текст.',
'timezone' => 'Атрибутът :attribute трябва да бъде валидна зона.',
'totp' => 'Предоставеният код не е валиден или е изтекъл.',
'unique' => 'Атрибутът :attribute вече е зает.',
'url' => 'Форматът на :attribute не е валиден.',
'uploaded' => 'Файлът не можа да бъде качен. Сървърът може да не приема файлове с такъв размер.',
// Custom validation lines
'custom' => [
'password-confirm' => [
'required_with' => 'Password confirmation required',
'required_with' => 'Изисква се потвърждение на паролата',
],
],

View File

@@ -24,6 +24,7 @@ return [
'width' => 'Width',
'height' => 'Height',
'More' => 'More',
'select' => 'Select...',
// Toolbar
'formats' => 'Formats',
@@ -52,9 +53,10 @@ return [
'align_left' => 'Align left',
'align_center' => 'Align center',
'align_right' => 'Align right',
'align_justify' => 'Align justify',
'align_justify' => 'Justify',
'list_bullet' => 'Bullet list',
'list_numbered' => 'Numbered list',
'list_task' => 'Task list',
'indent_increase' => 'Increase indent',
'indent_decrease' => 'Decrease indent',
'table' => 'Table',
@@ -91,7 +93,10 @@ return [
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
@@ -119,6 +124,16 @@ return [
'caption' => 'Caption',
'show_caption' => 'Show caption',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
@@ -139,12 +154,14 @@ return [
'toggle_label' => 'Toggle label',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',

View File

@@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Save Page',
'pages_title' => 'Page Title',
'pages_name' => 'Page Name',
@@ -225,6 +235,7 @@ return [
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Trenutna verzija',

View File

@@ -10,6 +10,8 @@ return [
'settings' => 'Settings',
'settings_save' => 'Save Settings',
'settings_save_success' => 'Settings saved',
'system_version' => 'System Version',
'categories' => 'Categories',
// App Settings
'app_customization' => 'Customization',
@@ -25,8 +27,8 @@ return [
'app_secure_images' => 'Higher Security Image Uploads',
'app_secure_images_toggle' => 'Enable higher security image uploads',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
'app_editor' => 'Page Editor',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Custom HTML Head Content',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
@@ -150,6 +152,7 @@ return [
'role_access_api' => 'Access system API',
'role_manage_settings' => 'Manage app settings',
'role_export_content' => 'Export content',
'role_editor_change' => 'Change page editor',
'role_asset' => 'Asset Permissions',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
@@ -275,6 +278,8 @@ return [
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti keel',
'eu' => 'Euskara',
'fa' => 'فارسی',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@@ -24,6 +24,7 @@ return [
'width' => 'Width',
'height' => 'Height',
'More' => 'More',
'select' => 'Select...',
// Toolbar
'formats' => 'Formats',
@@ -52,9 +53,10 @@ return [
'align_left' => 'Align left',
'align_center' => 'Align center',
'align_right' => 'Align right',
'align_justify' => 'Align justify',
'align_justify' => 'Justify',
'list_bullet' => 'Bullet list',
'list_numbered' => 'Numbered list',
'list_task' => 'Task list',
'indent_increase' => 'Increase indent',
'indent_decrease' => 'Decrease indent',
'table' => 'Table',
@@ -91,7 +93,10 @@ return [
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
@@ -119,6 +124,16 @@ return [
'caption' => 'Caption',
'show_caption' => 'Show caption',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
@@ -139,12 +154,14 @@ return [
'toggle_label' => 'Toggle label',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',

View File

@@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'Esborrany desat ',
'pages_edit_delete_draft' => 'Suprimeix l\'esborrany',
'pages_edit_discard_draft' => 'Descarta l\'esborrany',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'Defineix el registre de canvis',
'pages_edit_enter_changelog_desc' => 'Introduïu una breu descripció dels canvis que heu fet',
'pages_edit_enter_changelog' => 'Introduïu un registre de canvis',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Desa la pàgina',
'pages_title' => 'Títol de la pàgina',
'pages_name' => 'Nom de la pàgina',
@@ -225,6 +235,7 @@ return [
'pages_revisions_number' => 'Núm. ',
'pages_revisions_numbered' => 'Revisió núm. :id',
'pages_revisions_numbered_changes' => 'Canvis de la revisió núm. :id',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'Registre de canvis',
'pages_revisions_changes' => 'Canvis',
'pages_revisions_current' => 'Versió actual',

View File

@@ -10,6 +10,8 @@ return [
'settings' => 'Configuració',
'settings_save' => 'Desa la configuració',
'settings_save_success' => 'S\'ha desat la configuració',
'system_version' => 'System Version',
'categories' => 'Categories',
// App Settings
'app_customization' => 'Personalització',
@@ -25,8 +27,8 @@ return [
'app_secure_images' => 'Pujades d\'imatges amb més seguretat',
'app_secure_images_toggle' => 'Activa les pujades d\'imatges amb més seguretat',
'app_secure_images_desc' => 'Per motius de rendiment, totes les imatges són públiques. Aquesta opció afegeix una cadena aleatòria i difícil d\'endevinar al davant dels URL d\'imatges. Assegureu-vos que els índexs de directoris no estiguin activats per a evitar-hi l\'accés de manera fàcil.',
'app_editor' => 'Editor de pàgines',
'app_editor_desc' => 'Seleccioneu quin editor faran servir tots els usuaris per a editar les pàgines.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Contingut personalitzat a la capçalera HTML',
'app_custom_html_desc' => 'Aquí podeu afegir contingut que s\'inserirà a la part final de la secció <head> de cada pàgina. És útil per a sobreescriure estils o afegir-hi codi d\'analítiques.',
'app_custom_html_disabled_notice' => 'El contingut personalitzat a la capçalera HTML es desactiva en aquesta pàgina de la configuració per a assegurar que qualsevol canvi que trenqui el web es pugui desfer.',
@@ -150,6 +152,7 @@ return [
'role_access_api' => 'Accedeix a l\'API del sistema',
'role_manage_settings' => 'Gestiona la configuració de l\'aplicació',
'role_export_content' => 'Export content',
'role_editor_change' => 'Change page editor',
'role_asset' => 'Permisos de recursos',
'roles_system_warning' => 'Tingueu en compte que l\'accés a qualsevol dels tres permisos de dalt pot permetre que un usuari alteri els seus propis permisos o els privilegis d\'altres usuaris del sistema. Assigneu rols amb aquests permisos només a usuaris de confiança.',
'role_asset_desc' => 'Aquests permisos controlen l\'accés per defecte als recursos del sistema. Els permisos de llibres, capítols i pàgines tindran més importància que aquests permisos.',
@@ -275,6 +278,8 @@ return [
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti keel',
'eu' => 'Euskara',
'fa' => 'فارسی',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@@ -24,6 +24,7 @@ return [
'width' => 'Width',
'height' => 'Height',
'More' => 'More',
'select' => 'Select...',
// Toolbar
'formats' => 'Formats',
@@ -52,9 +53,10 @@ return [
'align_left' => 'Align left',
'align_center' => 'Align center',
'align_right' => 'Align right',
'align_justify' => 'Align justify',
'align_justify' => 'Justify',
'list_bullet' => 'Bullet list',
'list_numbered' => 'Numbered list',
'list_task' => 'Task list',
'indent_increase' => 'Increase indent',
'indent_decrease' => 'Decrease indent',
'table' => 'Table',
@@ -91,7 +93,10 @@ return [
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
@@ -119,6 +124,16 @@ return [
'caption' => 'Caption',
'show_caption' => 'Show caption',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
@@ -139,12 +154,14 @@ return [
'toggle_label' => 'Toggle label',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',

View File

@@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'Koncept uložen v ',
'pages_edit_delete_draft' => 'Odstranit koncept',
'pages_edit_discard_draft' => 'Zahodit koncept',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'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',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Uložit stránku',
'pages_title' => 'Nadpis stránky',
'pages_name' => 'Název stránky',
@@ -225,6 +235,7 @@ return [
'pages_revisions_number' => 'Č. ',
'pages_revisions_numbered' => 'Revize č. :id',
'pages_revisions_numbered_changes' => 'Změny revize č. :id',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'Protokol změn',
'pages_revisions_changes' => 'Změny',
'pages_revisions_current' => 'Aktuální verze',

View File

@@ -10,6 +10,8 @@ return [
'settings' => 'Nastavení',
'settings_save' => 'Uložit nastavení',
'settings_save_success' => 'Nastavení uloženo',
'system_version' => 'System Version',
'categories' => 'Categories',
// App Settings
'app_customization' => 'Přizpůsobení',
@@ -25,8 +27,8 @@ return [
'app_secure_images' => 'Nahrávat obrázky neveřejně a zabezpečeně',
'app_secure_images_toggle' => 'Zapnout bezpečnější nahrávání obrázků',
'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k přístup opět otevřelo.',
'app_editor' => 'Editor stránek',
'app_editor_desc' => 'Zvolte který editor budou užívat všichni uživatelé k úpravě stránek.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Vlastní obsah hlavičky HTML',
'app_custom_html_desc' => 'Cokoliv sem napíšete bude přidáno na konec sekce <head> v každém místě této aplikace. To se hodí pro přidávání nebo změnu CSS stylů nebo přidání kódu pro analýzu používání (např.: google analytics.).',
'app_custom_html_disabled_notice' => 'Na této stránce nastavení je zakázán vlastní obsah HTML hlavičky, aby bylo zajištěno, že bude možné vrátit případnou problematickou úpravu.',
@@ -150,6 +152,7 @@ return [
'role_access_api' => 'Přístup k systémovému API',
'role_manage_settings' => 'Správa nastavení aplikace',
'role_export_content' => 'Exportovat obsah',
'role_editor_change' => 'Change page editor',
'role_asset' => 'Obsahová oprávnění',
'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',
'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',
@@ -275,6 +278,8 @@ return [
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti keel',
'eu' => 'Euskara',
'fa' => 'فارسی',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@@ -24,6 +24,7 @@ return [
'width' => 'Width',
'height' => 'Height',
'More' => 'More',
'select' => 'Select...',
// Toolbar
'formats' => 'Formats',
@@ -52,9 +53,10 @@ return [
'align_left' => 'Align left',
'align_center' => 'Align center',
'align_right' => 'Align right',
'align_justify' => 'Align justify',
'align_justify' => 'Justify',
'list_bullet' => 'Bullet list',
'list_numbered' => 'Numbered list',
'list_task' => 'Task list',
'indent_increase' => 'Increase indent',
'indent_decrease' => 'Decrease indent',
'table' => 'Table',
@@ -91,7 +93,10 @@ return [
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
@@ -119,6 +124,16 @@ return [
'caption' => 'Caption',
'show_caption' => 'Show caption',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
// Images, links, details/summary & embed
'source' => 'Source',
@@ -139,12 +154,14 @@ return [
'toggle_label' => 'Toggle label',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',

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