mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 21:05:13 +08:00
⛅ Sync 2026-09-25 18:45:28
This commit is contained in:
1 parent
5a514b476e
commit
e624ec1984
32 files changed
+6359
-2
No files matched your search
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,91 @@
|
||||
#
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-footstrap-files
|
||||
# luci.mk keys hook names on LUCI_NAME, which defaults to the checkout's directory name — a
|
||||
# differently-named checkout symlinked into the feed would silently skip the Build/Prepare hook.
|
||||
LUCI_NAME:=luci-app-footstrap-files
|
||||
|
||||
# Version is git-derived; never hardcode PKG_VERSION. CI injects FOOTSTRAP_VERSION from the tag (an
|
||||
# SDK build has no .git, so it cannot derive one). This package has its own repo and its own tags:
|
||||
# the number here is this package's alone and does not track the theme's.
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=1
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=A file manager for the Footstrap theme
|
||||
|
||||
# +luci-base only, and NOT the theme. This is an ordinary LuCI view, not a chrome plugin: it renders
|
||||
# under any theme, and its stylesheet is scoped to this package's own `.fsf-*` classes, unlayered
|
||||
# and without one `!important` — an app's sheet already outranks every cascade layer a theme has
|
||||
# inside the app's own subtree. Depending on the theme would install a theme on a router that asked
|
||||
# for a file manager.
|
||||
#
|
||||
# cgi-io is what serves the actual bytes for an upload and a download (`/cgi-bin/cgi-upload`,
|
||||
# `/cgi-bin/cgi-download`); luci-base pulls it in already, and it is named here because this package
|
||||
# breaks without it rather than degrades.
|
||||
LUCI_DEPENDS:=+luci-base +cgi-io
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_MAINTAINER:=VizzleTF <vizzletf47@gmail.com>
|
||||
LUCI_URL:=https://github.com/VizzleTF/luci-app-footstrap-files
|
||||
LUCI_DESCRIPTION:=Browse, upload, download, rename, edit and delete files on the router. Written for \
|
||||
the Footstrap theme and usable under any other: the page is plain LuCI markup, the styling is scoped \
|
||||
to the package's own classes, and everything it does goes through luci-base's own `fs` API and cgi-io.
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
# CSS: ship verbatim. luci.mk defaults LUCI_MINIFY_CSS to 1, which runs csstidy over every *.css
|
||||
# under htdocs/ — and csstidy is fifteen years older than the syntax a sheet written today uses:
|
||||
# measured on the theme's own sheet it drops the `@layer` wrapper and every rule inside it, and this
|
||||
# one is full of color-mix(). The owfeed path minifies it with tools/minify-css.sh instead, which is
|
||||
# string-aware and checks its own rule count before and after.
|
||||
LUCI_MINIFY_CSS:=0
|
||||
|
||||
# RELOAD, NOT RESTART: rpcd holds sessions in memory, so `restart` logs out every LuCI user while
|
||||
# `reload` re-reads the ACLs and keeps them. This package DOES ship an acl.d file — unlike the
|
||||
# theme's other apps — so rpcd has something new to read on install, and a router where it is not
|
||||
# reloaded answers `access denied` until the next reboot.
|
||||
define Package/luci-app-footstrap-files/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-footstrap-files/postrm
|
||||
#!/bin/sh
|
||||
# An UPGRADE runs postrm for the old version after the new one is unpacked, so dropping the caches
|
||||
# is right in both cases, and reloading rpcd on the way out costs a session nothing.
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
# Absolute, not a relative path: CI rsyncs this package into package/, not into the feed.
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# luci.mk's Build/Prepare copies only src/ luasrc/ htdocs/ root/ ucode/ into $(PKG_BUILD_DIR), and
|
||||
# PKG_LICENSE_FILES resolves against THAT — so the Apache text has to be put there by hand or the
|
||||
# declaration above points at nothing. The hook name is keyed on LUCI_NAME, which is pinned at the
|
||||
# top for exactly this reason.
|
||||
define Build/Prepare/luci-app-footstrap-files
|
||||
$(CP) $(CURDIR)/LICENSE $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
# THE LINE BELOW IS LOAD-BEARING AND IT IS NOT A SIGNATURE — do not delete it as boilerplate.
|
||||
# include/scan.mk builds the package list by GREPPING Makefiles for `call BuildPackage`; luci.mk calls
|
||||
# it at the include above, but the grep cannot see that, so this literal is what puts the package into
|
||||
# the SDK's list.
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
+2040
File diff suppressed because it is too large.
Load diff
+118
@@ -0,0 +1,118 @@
|
||||
/* The editor's colours, taken from the theme instead of from GitHub.
|
||||
*
|
||||
* WHAT THIS REPLACES: prism-code-editor ships `themes/github-light.css` and `themes/github-dark.css`,
|
||||
* 6,492 bytes of literals between them, and the page picked one by asking the theme which way it was
|
||||
* and swapping the <link>. That is the minimum docs/luci-app-styling-guide.md §5 asks for — better
|
||||
* than mosdns and ssclash, which pin a dark editor onto a light page — but it is still somebody
|
||||
* else's palette on a Footstrap page, and it is two files where one will do.
|
||||
*
|
||||
* This is one file for both modes. Every colour comes from the 26-name export tier (§3.1), which
|
||||
* every LuCI theme publishes and which the theme itself recomputes when the reader switches to dark
|
||||
* — so the editor follows the page with no `isDark()`, no second stylesheet and no swap. Under a
|
||||
* theme that publishes nothing, the literal fallback on each `var()` is GitHub's own light value, so
|
||||
* the worst case is what shipped before.
|
||||
*
|
||||
* The palette is deliberately small: five accents and four text levels, because a config file is
|
||||
* keywords, strings, numbers and comments, and a router's screen is not the place to prove that
|
||||
* twelve hues fit on one line.
|
||||
*
|
||||
* ONLY THE NAMES THE VENDORED SHEETS ACTUALLY READ are defined. The library also understands
|
||||
* `--pce-ac-*` (autocomplete), `--pce-bg-fold`, `--pce-tabstop`, `--pce-invisibles` and
|
||||
* `--pce-guide-indent`; every one of those belongs to an extension this package does not load, and
|
||||
* a value nobody reads is a byte nobody needs. */
|
||||
|
||||
.prism-code-editor {
|
||||
/* the page's own surface, so the editor is part of the dialog rather than a window in it */
|
||||
--pce-bg: var(--background-color-high, #fff);
|
||||
--pce-cursor: var(--text-color-highest, #24292e);
|
||||
--pce-line-number: var(--text-color-low, #1b1f2380);
|
||||
--pce-border-highlight: var(--border-color-low, rgba(0, 0, 0, .12));
|
||||
|
||||
/* Three transient highlights, all tinted from the tier so they hold on either surface. The
|
||||
* literal is the fallback for a theme with no tier at all, and it is the value GitHub used. */
|
||||
--pce-bg-highlight: rgba(9, 105, 218, .06);
|
||||
--pce-bg-highlight: color-mix(in srgb, var(--primary-color-medium, #0969da) 6%, transparent);
|
||||
--pce-selection: rgba(9, 105, 218, .25);
|
||||
--pce-selection: color-mix(in srgb, var(--primary-color-medium, #0969da) 25%, transparent);
|
||||
--pce-selection-match: rgba(46, 125, 50, .28);
|
||||
--pce-selection-match: color-mix(in srgb, var(--success-color-medium, #2e7d32) 28%, transparent);
|
||||
--pce-search-match: rgba(224, 134, 0, .35);
|
||||
--pce-search-match: color-mix(in srgb, var(--warn-color-medium, #e08600) 35%, transparent);
|
||||
|
||||
/* the find-and-replace widget, which is chrome rather than code */
|
||||
--pce-widget-bg: var(--background-color-high, #f6f8fa);
|
||||
--pce-widget-bg-input: var(--background-color-low, #fafbfc);
|
||||
--pce-widget-border: var(--border-color-medium, #bfbfbf);
|
||||
--pce-widget-color: var(--text-color-high, #434d56);
|
||||
--pce-widget-color-active: var(--text-color-highest, #000);
|
||||
--pce-widget-color-options: var(--text-color-medium, #5a6772);
|
||||
--pce-widget-focus: var(--primary-color-medium, #0969da);
|
||||
--pce-widget-error: var(--error-color-medium, #be1100);
|
||||
--pce-widget-bg-hover: rgba(127, 127, 127, .12);
|
||||
--pce-widget-bg-hover: color-mix(in srgb, var(--text-color-medium, #666) 12%, transparent);
|
||||
--pce-widget-bg-active: rgba(9, 105, 218, .2);
|
||||
--pce-widget-bg-active: color-mix(in srgb, var(--primary-color-medium, #0969da) 20%, transparent);
|
||||
--pce-widget-bg-error: rgba(190, 17, 0, .15);
|
||||
--pce-widget-bg-error: color-mix(in srgb, var(--error-color-medium, #be1100) 15%, transparent);
|
||||
|
||||
color: var(--text-color-highest, #24292e);
|
||||
}
|
||||
|
||||
/* THE TOKENS THIS ROUTER ACTUALLY PRODUCES. Measured on the stand with /etc/config/*, a .json and a
|
||||
* shell script open: 19 classes, against the 40 the GitHub sheets paint. What is missing here is
|
||||
* missing because no grammar we ship emits it. */
|
||||
.prism-code-editor .token.comment,
|
||||
.prism-code-editor .token.shebang { color: var(--text-color-low, #6a737d); }
|
||||
|
||||
.prism-code-editor .token.string { color: var(--success-color-medium, #032f62); }
|
||||
|
||||
.prism-code-editor .token.keyword,
|
||||
.prism-code-editor .token.builtin,
|
||||
.prism-code-editor .token[class*="keyword-"] { color: var(--primary-color-medium, #d73a49); }
|
||||
|
||||
.prism-code-editor .token.number,
|
||||
.prism-code-editor .token.boolean { color: var(--warn-color-medium, #005cc5); }
|
||||
|
||||
/* `config interface 'lan'` — the name after the keyword is the thing being configured, so it takes
|
||||
* the strongest text level rather than an accent: it is the line's subject, not its syntax. */
|
||||
.prism-code-editor .token.property,
|
||||
.prism-code-editor .token.class-name { color: var(--text-color-highest, #24292e); }
|
||||
|
||||
.prism-code-editor .token.variable { color: var(--warn-color-high, #e36209); }
|
||||
|
||||
.prism-code-editor .token.operator,
|
||||
.prism-code-editor .token.punctuation { color: var(--text-color-medium, #24292e); }
|
||||
|
||||
/* Nesting depth, cycling through the tier's own accents. Six levels because that is what the
|
||||
* library emits before it starts again, and a config file rarely reaches three — but the level is
|
||||
* counted by matchBrackets over whatever the reader opens, not by our grammars, so the tail stays.
|
||||
*
|
||||
* `char`, `assign-left`, `important`, `bold` and `italic` were painted here until the bash grammar
|
||||
* left: nothing this package ships emits them any more, and a rule for a token that cannot appear
|
||||
* is a rule nobody needs. */
|
||||
.prism-code-editor .token.bracket-level-0,
|
||||
.prism-code-editor .token.bracket-level-6 { color: var(--primary-color-medium, #0366d6); }
|
||||
.prism-code-editor .token.bracket-level-1,
|
||||
.prism-code-editor .token.bracket-level-7 { color: var(--success-color-medium, #138934); }
|
||||
.prism-code-editor .token.bracket-level-2,
|
||||
.prism-code-editor .token.bracket-level-8 { color: var(--warn-color-medium, #b37700); }
|
||||
.prism-code-editor .token.bracket-level-3,
|
||||
.prism-code-editor .token.bracket-level-9 { color: var(--error-color-medium, #cb2431); }
|
||||
.prism-code-editor .token.bracket-level-4,
|
||||
.prism-code-editor .token.bracket-level-10 { color: var(--text-color-high, #a43276); }
|
||||
.prism-code-editor .token.bracket-level-5,
|
||||
.prism-code-editor .token.bracket-level-11 { color: var(--primary-color-high, #8a3ddb); }
|
||||
|
||||
.prism-code-editor .token.bracket-error { color: var(--error-color-high, #ff1212); }
|
||||
|
||||
/* The pair the caret is inside, drawn by extensions/matchBrackets. Inset rather than a background,
|
||||
* so the character keeps its own colour. */
|
||||
.prism-code-editor .active-bracket {
|
||||
box-shadow: inset 0 0 0 1px rgba(46, 125, 50, .6), inset 0 0 0 9in rgba(46, 125, 50, .25);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px color-mix(in srgb, var(--success-color-medium, #2e7d32) 60%, transparent),
|
||||
inset 0 0 0 9in color-mix(in srgb, var(--success-color-medium, #2e7d32) 25%, transparent);
|
||||
}
|
||||
|
||||
/* The line the caret is on gets its number at full strength — the library only swaps the variable. */
|
||||
.prism-code-editor .active-line { --pce-line-number: var(--text-color-high, #1f2328); }
|
||||
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require view.footstrap-files.grammars as grammars';
|
||||
|
||||
/* The editor, assembled by hand out of prism-code-editor rather than taken from its `setups`.
|
||||
*
|
||||
* WHY NOT `setups/index.js`. It is one import and it costs the whole library: measured on the stand
|
||||
* with the search widget and the history exercised, the ready-made setup fetched 263 FILES —
|
||||
* `languages/index.js` is a barrel that imports every grammar the project ships, zig and xojo
|
||||
* included. The pieces below are the same editor with the grammars this router needs, and nothing
|
||||
* is loaded until the reader opens a file.
|
||||
*
|
||||
* WHAT THE PACKAGE VENDORS, and why each file is there:
|
||||
* index.js + core-*.js the editor itself
|
||||
* prism/index.js + core-*.js the tokenizer
|
||||
* (no grammars at all) uci, shell and json are ours, in grammars.js: the library's bash
|
||||
* was 5,672 bytes of a language nobody writes in /etc/config, and its
|
||||
* json dragged a shared patterns module behind it for two regexes
|
||||
* extensions/search find and replace, which a config file needs and a textarea has not
|
||||
* extensions/matchBrackets the one thing that makes a nested config readable
|
||||
* layout.css, search.css the library's own layout and widget styling — the COLOURS are
|
||||
* ours, in editor.css, off the theme's export tier
|
||||
*
|
||||
* WHAT IS DELIBERATELY NOT HERE: `languages/<lang>.js`, the library's indent and comment rules.
|
||||
* They write into `languageMap`, and in this build NOBODY READS IT — the reader is
|
||||
* `extensions/commands`, which we do not load. Measured on the stand before removing them: Enter
|
||||
* inside a `{` produced a bare newline and Ctrl+/ did nothing, exactly as it does now. Four files
|
||||
* of two lines each pulled 3,970 bytes of shared and jsx-shared machinery behind them.
|
||||
*
|
||||
* Its stylesheets go into the editor's own SHADOW ROOT, so `<head>` is untouched and the theme's
|
||||
* cascade layer order cannot be inverted from here (footstrap docs/css.md). That is the property
|
||||
* that ruled out Ace, whose sheets land first in <head> unless `useStrictCSP` is set. */
|
||||
|
||||
/* E() WITH THE MARKUP SINK CLOSED: a primitive last argument becomes a text node, never
|
||||
* innerHTML. The reasoning, and the stand proof, are on the same shim in browser.js. */
|
||||
function E() {
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
const last = args.length - 1;
|
||||
if (last >= 1 && args[last] != null && typeof args[last] !== 'object' && typeof args[last] !== 'function')
|
||||
args[last] = [ args[last] ];
|
||||
return window.E.apply(null, args);
|
||||
}
|
||||
|
||||
/* Under `resources/view/<app>/`, which is where a LuCI app keeps what belongs to one view — the
|
||||
* shape stock luci-app-filemanager uses for its own HexEditor, and what the theme's app guide asks
|
||||
* for. A library in a SHARED path (`/luci-static/resources/codemirror/`, as AdGuardHome ships it) is
|
||||
* overwritten by the next app that vendors a different version of the same thing. */
|
||||
const V = L.resource('view/footstrap-files/vendor/pce');
|
||||
|
||||
/* UCI HAS ITS OWN GRAMMAR NOW. It used to be highlighted as shell, which was the closest thing the
|
||||
* library shipped and still wrong: `config interface 'lan'` has no keywords in bash, so a config
|
||||
* file came out as bare words with quoted strings. Prism's `ini` was worse — measured on
|
||||
* /etc/config/network, 9 tokens in 120 lines, because INI wants `key=value` and `[section]`.
|
||||
*
|
||||
* `ini` and `nginx` had grammars of their own and no longer do: a router that has nginx at all is
|
||||
* rare, `.ini` rarer still, and both are closer to shell than either was to uci.
|
||||
*
|
||||
* EVERY LANGUAGE HERE IS ONE grammars.js REGISTERS, so nothing is fetched and nothing can 404:
|
||||
* anything that is not uci or json is edited as shell. */
|
||||
|
||||
function languageFor(path) {
|
||||
if ((/^\/etc\/config\//).test(path)) return 'uci';
|
||||
return (/\.json$/i).test(path) ? 'json' : 'shell';
|
||||
}
|
||||
|
||||
let _loaded = null;
|
||||
|
||||
/* One load for the life of the page, whatever is opened afterwards. The modules are ES modules with
|
||||
* RELATIVE imports, so the browser resolves them straight from the router — no bundler runs in this
|
||||
* package's build, and none is needed. */
|
||||
function loadEditor() {
|
||||
if (_loaded) return _loaded;
|
||||
_loaded = Promise.all([
|
||||
import(`${V}/index.js`),
|
||||
import(`${V}/prism/index.js`),
|
||||
import(`${V}/extensions/search/index.js`),
|
||||
import(`${V}/extensions/matchBrackets/index.js`),
|
||||
]).then(([ core, prism, search, brackets ]) => {
|
||||
/* ours, registered against the tokenizer the library just brought in */
|
||||
grammars.register(prism.languages);
|
||||
return { core, prism, search, brackets };
|
||||
});
|
||||
return _loaded;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
/* Everything the caller needs: give it a container and a file, get an editor back. The container
|
||||
* keeps its own stylesheets, so nothing this returns leaks into the document. */
|
||||
open(container, path, text) {
|
||||
const lang = languageFor(path);
|
||||
return loadEditor().then((mods) => {
|
||||
const editor = mods.core.createEditor(container, {
|
||||
language: lang,
|
||||
value: text,
|
||||
lineNumbers: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: false, /* uci and shell are tab-indented on this platform */
|
||||
});
|
||||
editor.addExtensions(mods.search.searchWidget(), mods.brackets.matchBrackets());
|
||||
return editor;
|
||||
});
|
||||
},
|
||||
|
||||
/* The sheets the editor needs, as <link>s the CALLER owns: appended inside the view's tree, they
|
||||
* die with `#view` on the next navigation. */
|
||||
styles() {
|
||||
return [
|
||||
E('link', { rel: 'stylesheet', href: `${V}/layout.css` }),
|
||||
E('link', { rel: 'stylesheet', href: `${V}/search.css` }),
|
||||
/* OURS, not the library's: one sheet for both modes, on the export tier */
|
||||
E('link', { rel: 'stylesheet', href: L.resource('view/footstrap-files/editor.css') }),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,585 @@
|
||||
/* The file browser's own styling.
|
||||
*
|
||||
* The listing is an ordinary LuCI `<table class="table">` — the same markup Active DHCP Leases and
|
||||
* Associated Stations use — so the theme in use draws the rows, the hover, the sticky header and the
|
||||
* card stack a narrow column gets. What is left here is what no theme can know: that a mode string
|
||||
* is a monospace column, that the owner is secondary to it, and how a toolbar of eight controls
|
||||
* behaves when the column is narrow.
|
||||
*
|
||||
* COLOUR COMES FROM THE EXPORT TIER — the 26 `--*-color-*` names every LuCI theme publishes — with a
|
||||
* literal fallback on every one. Footstrap's own `--fs-*` properties are its PRIVATE tier: better
|
||||
* named, and not a contract (docs/luci-app-styling-guide.md §3.2). Reading them would tie this
|
||||
* package to one theme and break the day the theme renames one. Spacing, radius and type are not on
|
||||
* the tier at all, so they are plain values here.
|
||||
*
|
||||
* Everything is scoped to `.fsf-*` classes of this package's own. Nothing here is layered, and it
|
||||
* needs no `!important`: an app's sheet is unlayered and already outranks every cascade layer the
|
||||
* theme has inside the app's own subtree. */
|
||||
|
||||
/* ---- the toolbar --------------------------------------------------------------------------------
|
||||
*
|
||||
* A path, a Go, and six actions. It wraps rather than scrolls: a scrolling toolbar hides its own
|
||||
* buttons, and on a phone the row that wraps is the one a thumb can still reach. */
|
||||
/* Icon-only buttons: the icon is centred in the same box the text buttons had, so the bar keeps one
|
||||
* height whichever theme is under it. */
|
||||
.fsf-bar > .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fsf-bicon { display: block; }
|
||||
|
||||
.fsf-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
/* The path is the widest thing in the bar and the only one worth growing. Monospace, because it is a
|
||||
* path: `/etc/rc.d/S95done` reads wrong in a proportional face. */
|
||||
.fsf-bar > input[type="text"] {
|
||||
flex: 1 1 16rem;
|
||||
min-width: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
/* ---- where the reader is ------------------------------------------------------------------------
|
||||
*
|
||||
* The crumbs sit on the recessed surface so the path reads as chrome rather than as content, and
|
||||
* break anywhere: a deep path must not widen the page. */
|
||||
.fsf-crumbs {
|
||||
margin-bottom: .5rem;
|
||||
padding: .375rem .625rem;
|
||||
border-radius: 4px;
|
||||
background: var(--background-color-low, #f5f5f5);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: .8125rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.fsf-crumbs a { color: var(--primary-color-medium, #0069d9); text-decoration: none; }
|
||||
.fsf-crumbs a:hover { text-decoration: underline; }
|
||||
|
||||
/* ---- the count in the toolbar ---------------------------------------------------------------
|
||||
*
|
||||
* Select mode and a full clipboard both take the toolbar over and both say how many, so the count
|
||||
* is one class. It does not shrink: a wrapping toolbar may put it on a line of its own, and a
|
||||
* number that has been squeezed to "3 it…" answers nothing. */
|
||||
/* ---- the name cell ------------------------------------------------------------------------------
|
||||
*
|
||||
* The flex box is the inner span and NOT the `<td>`: a table cell told to be a flex container leaves
|
||||
* the table's row layout, and the name then rides a few pixels above every other cell in its row —
|
||||
* measured at 1280 as a name overlapping the header above it. */
|
||||
.fsf-name { overflow-wrap: anywhere; }
|
||||
|
||||
.fsf-nameline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.fsf-nameline input[type="checkbox"] { flex: none; margin: 0; }
|
||||
|
||||
/* A directory is where the reader is going next, so it takes the accent and the weight; a file is a
|
||||
* leaf and stays plain text. */
|
||||
.fsf-nameline a {
|
||||
color: var(--primary-color-medium, #0069d9);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fsf-nameline a:hover { text-decoration: underline; }
|
||||
|
||||
/* ---- permissions --------------------------------------------------------------------------------
|
||||
*
|
||||
* Tabular figures in a monospace face, so a column of modes lines up character by character and a
|
||||
* missing bit is visible without reading the string. The owner is one text level quieter: it answers
|
||||
* a second question, and at this table's density it competes with the mode otherwise. Both levels
|
||||
* clear AA on every surface — that is what the tier guarantees. */
|
||||
.fsf-mode {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fsf-owner {
|
||||
color: var(--text-color-medium, #666);
|
||||
font-size: .8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---- the actions --------------------------------------------------------------------------------
|
||||
*
|
||||
* Four buttons in the narrowest column of the table. They wrap instead of widening it. */
|
||||
.fsf-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .25rem;
|
||||
}
|
||||
|
||||
/* ---- the editor dialog ---------------------------------------------------------------------------
|
||||
*
|
||||
* The editor opens in LuCI's own modal. The dialog is sized for a form by default — around 30rem —
|
||||
* and a config file read at that width is worse than one with no highlighting, so the width is
|
||||
* asked for here and clamped by the viewport rather than fixed: `min()` keeps it inside a phone.
|
||||
*
|
||||
* Scoped by a class of this package's own, so nothing here reaches another app's dialog. */
|
||||
.modal.fsf-modal-dialog { width: min(64rem, 94vw); max-width: min(64rem, 94vw); }
|
||||
|
||||
/* A FLEX ROW, not `text-align: right` on a line of inline-blocks. The buttons sat on the text
|
||||
* baseline of the status span, so the icon button — the one whose box is set by its own content
|
||||
* rather than by a word — landed a pixel low and looked misaligned next to Save and Close
|
||||
* (measured: 621px against 620px). A flex row aligns the boxes themselves and the pixel goes away.
|
||||
* `wrap` because at the phone's width four controls do not fit on one line. */
|
||||
.fsf-modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
/* FIND IS AN ICON IN A ROW OF WORDS, so it needs the box the words get for free: LuCI sizes a button
|
||||
* by its content, and a button whose only child is a 20px <svg> comes out 20px wide next to a 60px
|
||||
* "Save". inline-flex centres the icon inside whatever padding the theme gives the button, and the
|
||||
* minimum is what keeps it a target rather than a glyph — 2.25rem under a mouse, and 2.75rem (44px,
|
||||
* the size Apple's HIG asks for; WCAG 2.5.8 asks 24 CSS px) wherever the pointer is coarse, which is
|
||||
* the case this button exists for.
|
||||
*
|
||||
* `pointer: coarse` and not a width query: a tablet with a keyboard attached is a wide screen with a
|
||||
* finger on it, and a narrow desktop window is not a touch screen. */
|
||||
.fsf-modal-actions .btn.fsf-find {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.25rem;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.fsf-modal-actions .btn.fsf-find { min-width: 2.75rem; min-height: 2.75rem; }
|
||||
}
|
||||
|
||||
.fsf-editor-status { color: var(--text-color-medium, #666); margin-inline-end: .5rem; }
|
||||
|
||||
/* `min(60vh, 32rem)`: tall enough for a config file, never taller than the viewport it is in — an
|
||||
* editor whose bottom needs the page scrolled is worse than a short one. */
|
||||
/* `min(65vh, 34rem)`: as tall as the dialog can be without the reader having to scroll the dialog
|
||||
* itself to reach the buttons under it — the overlay is the scroll container, and a box taller than
|
||||
* the viewport puts Save below the fold. */
|
||||
.fsf-editor {
|
||||
height: min(65vh, 34rem);
|
||||
border: 1px solid var(--border-color-low, rgba(0, 0, 0, .12));
|
||||
border-radius: 6px;
|
||||
overflow: hidden; /* the rounding; the scrolling belongs to the editor itself, below */
|
||||
}
|
||||
|
||||
/* THE HEIGHT GOES TO THE EDITOR, NOT ONLY TO THE BOX AROUND IT. The library's root grows to its
|
||||
* content — measured on a 300-line file: 5487px of editor inside a 542px box — so a wrapper that
|
||||
* merely clips leaves the reader with a file that has no scrollbar and a wheel that does nothing.
|
||||
* Given the height, the editor scrolls itself, which is also what keeps its line numbers and its
|
||||
* search widget where they belong: both are positioned inside that scroller, and scrolling the
|
||||
* WRAPPER instead would carry the search box off the top of the file.
|
||||
*
|
||||
* The face is the page's mono stack rather than the library's default. */
|
||||
.fsf-editor .prism-code-editor {
|
||||
height: 100%;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
/* ---- the editor's search widget ------------------------------------------------------------------
|
||||
*
|
||||
* The widget is prism-code-editor's, and its own sheet (vendor/pce/search.css) styles it for a page
|
||||
* that leaves plain `<input>` and `<button>` alone. LuCI does not: a theme sizes every input on the
|
||||
* page, and inside a widget whose rows are 1.5em tall the result is a field 32px high in a 20px box
|
||||
* — the Replace field spilling over its own border, with the theme's 3px focus ring drawn around the
|
||||
* spill (measured on the stand: height 32px, box-shadow 0 0 0 3px). What is undone here is exactly
|
||||
* that: the widget's fields go back to being the widget's.
|
||||
*
|
||||
* Scoped under `.fsf-editor`, so this reaches only the editor this package opens. Nothing here
|
||||
* restyles the widget — every value is the one vendor/pce/search.css already asks for. */
|
||||
.fsf-editor .prism-search input {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* The ring belongs to `.pce-input:focus-within`, which the library draws as a box-shadow on the
|
||||
* FIELD's container. A second ring on the input inside it is the theme answering the same question
|
||||
* twice, and it is the one that overflows. */
|
||||
.fsf-editor .prism-search input:focus,
|
||||
.fsf-editor .prism-search input:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* One face for the whole widget. The library asks for Arial on the two fields and lets everything
|
||||
* else inherit — which, inside an editor set in a monospace stack, put "Replace" and "All" in a
|
||||
* different face and a different width from the field beside them. */
|
||||
.fsf-editor .prism-search,
|
||||
.fsf-editor .prism-search .pce-input,
|
||||
.fsf-editor .prism-search .pce-match-count {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* A theme's minimum sizes are meant for a page's own buttons; here they push a 1.2em icon button
|
||||
* into a row built around 1.5em. The library sets every one of these explicitly, so the reset is
|
||||
* back to its values rather than to nothing. */
|
||||
.fsf-editor .prism-search button {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.fsf-editor .prism-search .pce-input button { margin-left: 1px; }
|
||||
|
||||
/* ---- the hex editor -------------------------------------------------------------------------
|
||||
*
|
||||
* Three columns of monospace: the offset, sixteen bytes, and what those bytes say if they say
|
||||
* anything. The box scrolls itself — the lines inside it are drawn for the window only, so the
|
||||
* scrollbar belongs to a spacer of the full height rather than to the content.
|
||||
*
|
||||
* `tabular-nums` and a fixed cell width, because a hex dump whose columns do not line up is worth
|
||||
* less than no hex dump: the eye reads the grid, not the digits. */
|
||||
.fsf-hex {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: .8125rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.5;
|
||||
background: var(--background-color-high, #fff);
|
||||
}
|
||||
|
||||
.fsf-hex-spacer { position: relative; }
|
||||
.fsf-hex-layer { position: absolute; top: 0; left: 0; right: 0; will-change: transform; }
|
||||
|
||||
.fsf-hex-line { display: flex; gap: .75rem; padding: 0 .5rem; white-space: pre; }
|
||||
|
||||
.fsf-hex-off { flex: none; color: var(--text-color-low, #999); }
|
||||
.fsf-hex-bytes { flex: none; }
|
||||
.fsf-hex-text { color: var(--text-color-medium, #666); }
|
||||
|
||||
/* Each byte is its own box so the caret can sit on one, and so a click knows which. The width is
|
||||
* two characters plus the gap the eye needs between pairs. */
|
||||
.fsf-hex-b {
|
||||
display: inline-block;
|
||||
width: 1.6em;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* An eighth of the width between the two halves of the line, the way od(1) breaks it up. */
|
||||
.fsf-hex-b:nth-child(8n) { margin-right: .5em; }
|
||||
|
||||
.fsf-hex-at {
|
||||
background: var(--primary-color-medium, #0069d9);
|
||||
color: var(--on-primary-color, #fff);
|
||||
}
|
||||
|
||||
/* A byte the reader typed, so an unsaved change is visible without diffing anything. */
|
||||
.fsf-hex-edited { color: var(--error-color-medium, #c9302c); font-weight: 700; }
|
||||
.fsf-hex-at.fsf-hex-edited { color: var(--on-primary-color, #fff); }
|
||||
|
||||
/* ---- the card stack -----------------------------------------------------------------------------
|
||||
*
|
||||
* At the width where a theme turns the table into one card per file, the name becomes the card's
|
||||
* heading and the actions its footer. Both are keyed off `.fs-stacked` — the class Footstrap puts on
|
||||
* a carded table — and pinned by a class of this package's own, so under any other theme they are
|
||||
* inert rather than wrong.
|
||||
*
|
||||
* A card is a wrapping flex row whose cells are held to pairs by a flagged rule in the theme. That
|
||||
* flag is not one this sheet fights: the theme exempts `[class*="col-"]` from it on purpose, as the
|
||||
* view author's own say over a carded row, so the two cells that want a full line carry `col-10` in
|
||||
* the markup and take their width from here. */
|
||||
.table.fs-stacked .fsf-name,
|
||||
.table.fs-stacked .fsf-actions { flex: 0 0 100%; }
|
||||
|
||||
/* An EMPTY cell prints its caption and nothing under it. In a table that is a blank in a column the
|
||||
* reader is scanning anyway; in a card it is a labelled void — the `..` row alone produced three
|
||||
* (Permissions, Modified, Actions). Hidden in the card only: a table needs every cell present for
|
||||
* its columns to line up. */
|
||||
.table.fs-stacked .fsf-row .td:empty { display: none; }
|
||||
|
||||
/* The name is the card's heading, so it drops the eyebrow the theme prints above every other value:
|
||||
* a card captioned "Name" over the file name says nothing the reader did not know, and costs the
|
||||
* heading its first line. */
|
||||
.table.fs-stacked .fsf-name::before { content: none; }
|
||||
|
||||
.table.fs-stacked .fsf-name {
|
||||
margin-bottom: .25rem;
|
||||
padding-bottom: .5rem;
|
||||
border-bottom: 1px solid var(--border-color-low, rgba(0, 0, 0, .12));
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table.fs-stacked .fsf-actions {
|
||||
margin-top: .25rem;
|
||||
padding-top: .5rem;
|
||||
border-top: 1px solid var(--border-color-low, rgba(0, 0, 0, .12));
|
||||
}
|
||||
|
||||
.table.fs-stacked .fsf-actions .btn { flex: 1 1 auto; }
|
||||
|
||||
/* ---- the status line ----------------------------------------------------------------------------
|
||||
*
|
||||
* What the page is doing right now, in the toolbar rather than as a notification: an upload of six
|
||||
* files would otherwise print six banners and push the listing off the screen. */
|
||||
.fsf-status {
|
||||
color: var(--text-color-medium, #666);
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
/* ---- drag and drop ------------------------------------------------------------------------------
|
||||
*
|
||||
* The target says so before the file is let go. An outline rather than a background: the row keeps
|
||||
* its own striping, and a filled highlight over a table row reads as "selected", which this is not. */
|
||||
.fsf-drop {
|
||||
outline: 2px dashed var(--primary-color-medium, #0069d9);
|
||||
outline-offset: -2px;
|
||||
background: var(--background-color-low, #f5f5f5);
|
||||
}
|
||||
|
||||
/* ---- the tile view ------------------------------------------------------------------------------
|
||||
*
|
||||
* The other way to read a directory: one tile per entry. `auto-fill` with a floor rather than a
|
||||
* fixed column count, so the same rule gives four across a phone and a dozen across a desktop
|
||||
* without a media query — and it measures the CONTAINER, which is what the theme's own guide asks
|
||||
* for (the viewport is not the column). */
|
||||
/* 6.5rem is measured, not chosen: the grid gets `clientWidth` 343 / 358 / 361 px on the three
|
||||
* iPhone widths (375, 390, 393) with the theme's own gutters taken off, and with an 8px gap a
|
||||
* minimum of 6.5rem (104px) is the largest that still fits THREE across all of them — 7rem drops
|
||||
* the 375 back to two, 7.5rem (what this was) gave two on every iPhone but the Pro Max. A 320px
|
||||
* screen keeps two, and that is right: a third column there is 90px wide, which is narrower than
|
||||
* the icon plus a name. */
|
||||
.fsf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
||||
gap: .5rem;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
||||
.fsf-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
padding: .625rem .375rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fsf-tile:hover { background: var(--background-color-low, #f5f5f5); }
|
||||
.fsf-tile:focus-visible { outline: 2px solid var(--primary-color-medium, #0069d9); outline-offset: 2px; }
|
||||
|
||||
/* Selected is a filled surface plus a border, not a colour on the text: a name is already the
|
||||
* accent when it is a directory, and colour alone would make the two states indistinguishable. */
|
||||
.fsf-tile-sel {
|
||||
border-color: var(--primary-color-medium, #0069d9);
|
||||
/* The same tint the selected row carries, so the two views say "selected" the same way. The
|
||||
* literal is what a browser without color-mix() gets. */
|
||||
background: rgba(9, 105, 218, .12);
|
||||
background: color-mix(in srgb, var(--primary-color-medium, #0969da) 12%, transparent);
|
||||
}
|
||||
|
||||
/* The ink is set per shape in the markup, from the export tier; what is left here is the box. The
|
||||
* icon never shrinks — a long file name must eat the name's own line, not the glyph. */
|
||||
.fsf-icon { flex: none; vertical-align: -.25em; }
|
||||
.fsf-tile-name { overflow-wrap: anywhere; line-height: 1.25; }
|
||||
.fsf-tile-meta { color: var(--text-color-medium, #666); font-size: .75rem; }
|
||||
|
||||
/* THE LISTING KEEPS A FLOOR UNDER ITSELF. A table ends exactly where its last row does — measured,
|
||||
* the listing box and the table box were the same 279px — so a directory with four files left the
|
||||
* reader nowhere to START a rubber band except on a row, where a press means dragging that row
|
||||
* instead. The floor is what every desktop file manager has under its list: empty space that
|
||||
* belongs to the listing.
|
||||
*
|
||||
* `min()` rather than a fixed height: on a phone 26rem of empty space below three files is a page
|
||||
* of nothing to scroll past. */
|
||||
.fsf-listing { min-height: min(50vh, 26rem); }
|
||||
|
||||
/* THE RUBBER BAND, drawn in viewport coordinates on `document.body` — `position: fixed` so it needs
|
||||
* no arithmetic against the page's scroll, and `pointer-events: none` so the drag underneath it
|
||||
* keeps reaching the rows. It exists only while the button is down. */
|
||||
.fsf-band {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
border: 1px solid var(--primary-color-medium, #0069d9);
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 105, 217, .12);
|
||||
background: color-mix(in srgb, var(--primary-color-medium, #0069d9) 12%, transparent);
|
||||
}
|
||||
|
||||
/* A row being dragged over. The same class the upload drop already used, so a move and an upload
|
||||
* land on the same affordance — what changes is the cursor, which the drag itself sets. */
|
||||
|
||||
/* ---- the context menu ---------------------------------------------------------------------------
|
||||
*
|
||||
* Positioned from the pointer and clamped to the window by the JS. `z-index: 100` and no higher:
|
||||
* a theme's own overlays and dropdowns have to be able to sit above this, and the four-digit
|
||||
* z-indexes in the wild are how an app ends up painting over a modal it did not know about. */
|
||||
.fsf-menu {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
min-width: 11rem;
|
||||
padding: .25rem;
|
||||
border: 1px solid var(--border-color-medium, #ccc);
|
||||
border-radius: 8px;
|
||||
background: var(--background-color-high, #fff);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
|
||||
}
|
||||
|
||||
.fsf-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .375rem .625rem;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fsf-menu-item:hover,
|
||||
.fsf-menu-item:focus-visible { background: var(--background-color-low, #f5f5f5); }
|
||||
|
||||
/* The one destructive entry carries the error ink from the tier, and takes its own ink when filled
|
||||
* — never a hardcoded white, which is the failure the tier exists to prevent. */
|
||||
.fsf-menu-danger { color: var(--error-color-medium, #c0392b); }
|
||||
.fsf-menu-danger:hover { background: var(--error-color-medium, #c0392b); color: var(--on-error-color, #fff); }
|
||||
|
||||
.fsf-menu-sep {
|
||||
height: 1px;
|
||||
margin: .25rem .25rem;
|
||||
background: var(--border-color-low, rgba(0, 0, 0, .12));
|
||||
}
|
||||
|
||||
/* ---- the path box's completions ---------------------------------------------------------------
|
||||
*
|
||||
* THE CONTEXT MENU'S OWN SURFACE, by carrying `.fsf-menu` as well as this class: a floating list of
|
||||
* choices already has a description in this sheet, and a second copy of it is a second thing to
|
||||
* keep in step with the theme. What this class adds is the two things a completion list needs and
|
||||
* a menu does not — a ceiling on its height, because `/usr/bin` is hundreds of entries, and a width
|
||||
* set from the box it belongs to by the JS.
|
||||
*
|
||||
* It replaced a native <datalist>, which no stylesheet can reach: the browser draws that one in the
|
||||
* system's font and the system's colours, so on a dark page it came up as a white popup under a
|
||||
* dark toolbar. */
|
||||
.fsf-suggest {
|
||||
max-height: 15rem;
|
||||
overflow-y: auto;
|
||||
/* The page does not scroll on behind the list once it has hit its own end. */
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* The arrow keys mark an entry rather than writing it into the box, so the mark has to be visible
|
||||
* without the pointer: the same wash `:hover` uses, because it means the same thing. */
|
||||
.fsf-suggest .fsf-menu-item[aria-selected="true"] { background: var(--background-color-low, #f5f5f5); }
|
||||
|
||||
/* The directory the reader has already typed is context; the name is what they are choosing. */
|
||||
.fsf-suggest-dir { color: var(--text-color-medium, #666); }
|
||||
|
||||
/* The caption for it, hidden wherever a right button exists. */
|
||||
.fsf-hint {
|
||||
display: none;
|
||||
align-self: center;
|
||||
color: var(--text-color-medium, #666);
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
/* ---- touch ---------------------------------------------------------------------------------
|
||||
*
|
||||
* The long press that opens the context menu (browser.js `longPress`). Without the callout being
|
||||
* suppressed the system's own Copy / Look Up sheet opens on top of it, and without `user-select`
|
||||
* the press paints a text selection across the row on the way. Coarse pointers only: a mouse
|
||||
* reader keeps text selection and the browser's own menu everywhere else. */
|
||||
@media (pointer: coarse) {
|
||||
.fsf-row,
|
||||
.fsf-tile {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fsf-hint { display: inline; }
|
||||
}
|
||||
|
||||
/* ---- selecting ------------------------------------------------------------------------------
|
||||
*
|
||||
* One tap opens, so a row is a target rather than a form: the pointer says so, and the text under
|
||||
* it is not selectable — Shift+click across ten rows would otherwise paint a text selection over
|
||||
* the range it just made. The NAME stays selectable, because copying a file name is a thing
|
||||
* readers do. */
|
||||
.fsf-row,
|
||||
.fsf-tile {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* removes the 300 ms tap delay without disabling the pinch zoom `none` would take away */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.fsf-row .fsf-nameline a,
|
||||
.fsf-row .fsf-nameline span,
|
||||
.fsf-tile-name {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* A TINT, not the primary colour itself: `--primary-color-low` is a full-strength surface in some
|
||||
* themes — measured on this stand it painted the row navy and took the text and the action icons
|
||||
* with it. 12% of the primary over whatever the row already is keeps every one of them readable in
|
||||
* both light and dark. */
|
||||
/* On the ROW, and the cells stay transparent so it shows through them. Tinting the cells instead
|
||||
* left a 9px white band along the actions column: the theme lays that cell out as a flex box, and a
|
||||
* flex `td` takes its content's height (51px measured) rather than the row's (60px). */
|
||||
.table .tr.fsf-row.fsf-sel,
|
||||
.table .tr.fsf-row.fsf-sel:hover {
|
||||
background: rgba(9, 105, 218, .12);
|
||||
background: color-mix(in srgb, var(--primary-color-medium, #0969da) 12%, transparent);
|
||||
}
|
||||
|
||||
/* HOW MANY — for both states that take the toolbar over: select mode, where it sits where the path
|
||||
* box was, and a full clipboard, where it sits beside it. One class, because it answers the same
|
||||
* question. `flex: none` so a wrapping bar never squeezes a number down to an ellipsis. */
|
||||
.fsf-selcount,
|
||||
.fsf-clipcount {
|
||||
flex: none;
|
||||
margin-right: .25rem;
|
||||
align-self: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The heading a menu opened on a multiple selection carries, so the reader can see what Delete is
|
||||
* about to take. */
|
||||
.fsf-menu-head {
|
||||
padding: .35rem .75rem;
|
||||
color: var(--text-color-medium, #666);
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
/* The row's actions are icons too: the same box as the toolbar's, one size down. */
|
||||
.fsf-actions .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/* The two grammars this router actually needs, written here rather than vendored.
|
||||
*
|
||||
* WHAT THIS REPLACES: `prism/languages/bash.js`, 5,672 bytes of minified Prism grammar covering
|
||||
* process substitution, here-documents, arithmetic expansion and the rest of a language nobody
|
||||
* writes in /etc/config. It was also the WRONG grammar: uci is not shell, and the page has carried
|
||||
* a note saying so since the first commit — `config interface 'lan'` has no keywords in bash, so
|
||||
* the whole file came out as bare words with quoted strings, which is what the screenshots showed.
|
||||
*
|
||||
* A Prism grammar is an ordered object of regexes, and `languages.<name>` is where the tokenizer
|
||||
* looks — the same shape the vendored json grammar uses, and the reason this file needs nothing
|
||||
* from the library but that one export.
|
||||
*
|
||||
* ORDER IS THE GRAMMAR. Prism tries each pattern in turn over what is left, so `comment` before
|
||||
* `string` (a `#` inside quotes must not open one — hence `string` first for shell, where `$'…'`
|
||||
* and `"…#…"` are ordinary) and `keyword` before the bare-word fallbacks.
|
||||
*
|
||||
* NO LOOKBEHIND, anywhere. Safari only learned it in 16.4 and this package's floor is 15.4, so a
|
||||
* name that follows a keyword is matched as one token with an `inside`, not with `(?<=…)`. */
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
/* ---- uci -----------------------------------------------------------------------------------
|
||||
*
|
||||
* Three keywords, and everything else is a name or a value:
|
||||
*
|
||||
* config interface 'lan' section: keyword + type, then an optional name
|
||||
* option proto 'static' entry: keyword + option name, then the value
|
||||
* list ports 'eth0'
|
||||
*
|
||||
* The type after `config` is the SUBJECT of the block — what is being configured — so it takes
|
||||
* `class-name`, which editor.css paints at the strongest text level rather than as syntax. */
|
||||
const UCI = {
|
||||
comment: { pattern: /#.*/g, greedy: true },
|
||||
/* keyword + the word after it, in one match, so the second word can be typed by position
|
||||
* without a lookbehind */
|
||||
section: {
|
||||
pattern: /^[ \t]*config[ \t]+[\w.-]+/gm,
|
||||
inside: {
|
||||
keyword: /config/,
|
||||
'class-name': /[\w.-]+$/,
|
||||
},
|
||||
},
|
||||
entry: {
|
||||
pattern: /^[ \t]*(?:option|list)[ \t]+[\w.-]+/gm,
|
||||
inside: {
|
||||
keyword: /option|list/,
|
||||
property: /[\w.-]+$/,
|
||||
},
|
||||
},
|
||||
string: { pattern: /'[^'\n]*'|"(?:\\.|[^"\\\n])*"/g, greedy: true },
|
||||
number: /\b\d+(?:\.\d+)*\b/g,
|
||||
/* an unquoted value — uci writes plenty of them */
|
||||
variable: /\S+/g,
|
||||
};
|
||||
|
||||
/* ---- json ----------------------------------------------------------------------------------
|
||||
*
|
||||
* The vendored one was eight regexes and pulled `patterns-DSInPV_c.js` behind it for two of them —
|
||||
* 1,070 bytes for a format whose whole grammar fits here. `property` before `string` because a key
|
||||
* is a string too, and the lookahead is what tells them apart.
|
||||
*
|
||||
* `webmanifest` is not aliased and `//` comments are not matched: JSON has no comments, and a file
|
||||
* on this router that uses them is JSON5, which is not what the tokenizer was asked for. */
|
||||
const JSON_ = {
|
||||
property: { pattern: /"(?:\\.|[^\\\n"])*"(?=\s*:)/g, greedy: true },
|
||||
string: { pattern: /"(?:\\.|[^\\\n"])*"/g, greedy: true },
|
||||
number: /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/gi,
|
||||
boolean: /\b(?:false|true)\b/g,
|
||||
null: { pattern: /\bnull\b/g, alias: 'keyword' },
|
||||
operator: /:/g,
|
||||
punctuation: /[[\]{},]/g,
|
||||
};
|
||||
|
||||
/* ---- shell ---------------------------------------------------------------------------------
|
||||
*
|
||||
* Enough for an init script, a hotplug hook and the `.conf` files that are really shell: the
|
||||
* shebang, comments, both quotings, expansions, the control words and the builtins a router's
|
||||
* scripts are made of. What is deliberately absent is everything Prism's bash grammar carries for
|
||||
* an interactive shell — here-docs, process substitution, arithmetic — which no file under /etc on
|
||||
* this box uses and which cost 5 KB to describe. */
|
||||
const VAR = /\$(?:\{[^}\n]*\}|[\w@#?*!$-]+)/g;
|
||||
|
||||
const SHELL = {
|
||||
shebang: { pattern: /^#!.*/, alias: 'comment' },
|
||||
comment: { pattern: /(?:^|[ \t])#.*/g, greedy: true },
|
||||
/* SINGLE AND DOUBLE QUOTES ARE DIFFERENT LANGUAGES. `'$HOME'` is four characters; `"$HOME"` is
|
||||
* an expansion, and a shell script on a router is mostly the second kind — so the double-quoted
|
||||
* form carries an `inside` that keeps painting variables, while the single-quoted one is one
|
||||
* flat string. Without this every `"$X"` came out as a plain string and the reader lost the one
|
||||
* thing worth seeing in `[ "$X" = 1 ]`. */
|
||||
string: { pattern: /'[^']*'/g, greedy: true },
|
||||
dquote: {
|
||||
pattern: /"(?:\\.|[^"\\])*"/g,
|
||||
greedy: true,
|
||||
alias: 'string',
|
||||
inside: { variable: VAR },
|
||||
},
|
||||
variable: VAR,
|
||||
keyword: /\b(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|function|return|local|export|readonly|break|continue|exit|trap)\b/g,
|
||||
builtin: /\b(?:echo|printf|test|cd|read|set|unset|eval|exec|shift|source|sleep|kill|logger|uci|ubus|opkg|apk|service|reload_config)\b/g,
|
||||
number: /\b\d+\b/g,
|
||||
operator: /(?:&&|\|\||[|&;]|[!=<>]=?|=)/g,
|
||||
punctuation: /[{}[\]()]/g,
|
||||
};
|
||||
|
||||
return baseclass.extend({
|
||||
/* Registered against the tokenizer the editor already loaded, so this adds a module and no
|
||||
* dependency: `languages` is the one thing the vendored prism entry point exports that anyone
|
||||
* outside it needs. */
|
||||
register(languages) {
|
||||
languages.uci = UCI;
|
||||
languages.shell = SHELL;
|
||||
languages.json = JSON_;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require dom';
|
||||
|
||||
/* A hex editor, in about two hundred lines.
|
||||
*
|
||||
* WHY NOT THE STOCK ONE. luci-app-filemanager ships 40,660 bytes of HexEditor: ASCII, hex and
|
||||
* RegExp search, a settings panel for its own padding, and a `<style>` element injected into the
|
||||
* document — which outlives the page that added it and repaints the next one. Half of that is the
|
||||
* search. This is the other half: look at the bytes, change them, save them.
|
||||
*
|
||||
* VIRTUAL SCROLLING IS NOT AN OPTIMISATION HERE, it is the only way the thing works. At 16 bytes a
|
||||
* line a 1 MB file is 65,536 lines; building them costs seconds and holding them costs a phone its
|
||||
* tab. What exists instead is a spacer of the full height and a window of the lines actually on
|
||||
* screen, redrawn on scroll.
|
||||
*
|
||||
* THE FILE IS NOT A STRING. It arrives as a Blob through cgi-io (`fs.read_direct`, which is what
|
||||
* this page already uses to download) and lives as a Uint8Array; nothing here decodes it, because
|
||||
* the whole point is the bytes that are not text. The caller reads them back with `value()`. */
|
||||
|
||||
/* E() WITH THE MARKUP SINK CLOSED: a primitive last argument becomes a text node, never
|
||||
* innerHTML. The reasoning, and the stand proof, are on the same shim in browser.js. */
|
||||
function E() {
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
const last = args.length - 1;
|
||||
if (last >= 1 && args[last] != null && typeof args[last] !== 'object' && typeof args[last] !== 'function')
|
||||
args[last] = [ args[last] ];
|
||||
return window.E.apply(null, args);
|
||||
}
|
||||
|
||||
/* ---- fill(), THE SAME SINK ONE DOOR ALONG -----------------------------------------------------
|
||||
*
|
||||
* `E()` IS ONLY ONE OF dom.append's CALLERS, and the shim above therefore closes only one of the
|
||||
* doors. `dom.content(node, children)` empties the node and hands the children to that same
|
||||
* function, so its string branch is the same `node.innerHTML = ${children}` — reached without
|
||||
* `E()` ever being called, and invisible to a shim that wraps E's last argument.
|
||||
*
|
||||
* A FUNCTION IS THE SINK AT ONE REMOVE. dom.append does
|
||||
*
|
||||
* else if (typeof(children) === 'function') { return this.append(node, children(node)); }
|
||||
*
|
||||
* — it CALLS what it is given and recurses on the result, so `fill(node, () => entry.name)` would
|
||||
* land a file name on innerHTML just as a bare string does. Both are normalised here.
|
||||
*
|
||||
* Nothing in this module passes either shape today. This is what keeps it that way, in the shape
|
||||
* the E() shim already established: one function, not a rule to be remembered at every call site.
|
||||
* Anything that is not an array and not a node becomes a ONE-ELEMENT ARRAY — the branch that builds
|
||||
* text nodes — and `[ null ]` appends nothing, exactly as a bare `null` did.
|
||||
*
|
||||
* `dom.content` is called HERE AND NOWHERE ELSE in this module, the way `window.E` is reached only
|
||||
* inside the shim above. tools/dom-sinks.mjs holds both. */
|
||||
function fill(node, children) {
|
||||
return dom.content(node, (Array.isArray(children) || dom.elem(children)) ? children : [ children ]);
|
||||
}
|
||||
|
||||
const ROW = 16; /* bytes per line — the width every hex dump has had since od(1) */
|
||||
const OVERSCAN = 6; /* lines drawn above and below the window, so a fast scroll has cover */
|
||||
|
||||
const HEX = [];
|
||||
for (let i = 0; i < 256; i++) HEX.push(i.toString(16).padStart(2, '0'));
|
||||
|
||||
/* A byte is printable if it is a printable ASCII character; everything else is a dot, the way every
|
||||
* hex dump does it. Latin-1 would be prettier and a lie: the file has no encoding. */
|
||||
function ascii(b) {
|
||||
return (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.';
|
||||
}
|
||||
|
||||
/* The text column of the line that starts at `start`. */
|
||||
function textOf(data, start) {
|
||||
let s = '';
|
||||
for (let i = 0; i < ROW && start + i < data.length; i++) s += ascii(data[start + i]);
|
||||
return s;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
/* `container` is emptied and filled. Returns a handle whose `value()` is the bytes. */
|
||||
open(container, bytes) {
|
||||
const data = new Uint8Array(bytes);
|
||||
const lines = Math.max(1, Math.ceil(data.length / ROW));
|
||||
let caret = 0; /* byte the caret is on */
|
||||
let nibble = 0; /* 0 = the high half of that byte is next, 1 = the low half */
|
||||
let first = -1; /* first line currently drawn, so a scroll that moves nothing redraws nothing */
|
||||
|
||||
const layer = E('div', { class: 'fsf-hex-layer' });
|
||||
const spacer = E('div', { class: 'fsf-hex-spacer' }, layer);
|
||||
const view = E('div', { class: 'fsf-hex', tabindex: '0' }, spacer);
|
||||
|
||||
/* MEASURED, NOT ASSUMED. The line height is whatever the theme's monospace stack gives at
|
||||
* this size, and a guess would drift a pixel per line — 65,536 lines of drift. One line is
|
||||
* rendered off-screen to ask. */
|
||||
const probe = E('div', { class: 'fsf-hex-line' }, E('span', { class: 'fsf-hex-off' }, '00000000'));
|
||||
view.appendChild(probe);
|
||||
const LH = probe.getBoundingClientRect().height || 18;
|
||||
probe.remove();
|
||||
|
||||
spacer.style.height = (lines * LH) + 'px';
|
||||
|
||||
const line = (n) => {
|
||||
const start = n * ROW;
|
||||
const cells = [];
|
||||
for (let i = 0; i < ROW; i++) {
|
||||
const at = start + i;
|
||||
const has = at < data.length;
|
||||
cells.push(E('span', {
|
||||
class: 'fsf-hex-b' + (at === caret ? ' fsf-hex-at' : ''),
|
||||
'data-at': has ? String(at) : null,
|
||||
}, has ? HEX[data[at]] : ' '));
|
||||
}
|
||||
return E('div', { class: 'fsf-hex-line' }, [
|
||||
E('span', { class: 'fsf-hex-off' }, (start).toString(16).padStart(8, '0')),
|
||||
E('span', { class: 'fsf-hex-bytes' }, cells),
|
||||
E('span', { class: 'fsf-hex-text' }, textOf(data, start)),
|
||||
]);
|
||||
};
|
||||
|
||||
const draw = (force) => {
|
||||
const top = Math.max(0, Math.floor(view.scrollTop / LH) - OVERSCAN);
|
||||
if (!force && top === first) return;
|
||||
first = top;
|
||||
const count = Math.ceil(view.clientHeight / LH) + (OVERSCAN * 2);
|
||||
const out = [];
|
||||
for (let n = top; n < Math.min(lines, top + count); n++) out.push(line(n));
|
||||
layer.style.transform = 'translateY(' + (top * LH) + 'px)';
|
||||
fill(layer, out);
|
||||
};
|
||||
|
||||
/* The caret is a class on one cell, so moving it redraws nothing but the two cells involved
|
||||
* — until it leaves the window, which is the only time the lines are rebuilt. */
|
||||
const paint = () => {
|
||||
const old = layer.querySelector('.fsf-hex-at');
|
||||
if (old) old.classList.remove('fsf-hex-at');
|
||||
const now = layer.querySelector('[data-at="' + caret + '"]');
|
||||
if (now) { now.classList.add('fsf-hex-at'); return; }
|
||||
/* out of view: scroll it back in, which redraws */
|
||||
const target = Math.floor(caret / ROW);
|
||||
view.scrollTop = (target - Math.floor(view.clientHeight / LH / 2)) * LH;
|
||||
draw(true);
|
||||
const el = layer.querySelector('[data-at="' + caret + '"]');
|
||||
if (el) el.classList.add('fsf-hex-at');
|
||||
};
|
||||
|
||||
const move = (to) => {
|
||||
caret = Math.max(0, Math.min(data.length - 1, to));
|
||||
nibble = 0;
|
||||
paint();
|
||||
};
|
||||
|
||||
view.addEventListener('scroll', () => draw(false));
|
||||
view.addEventListener('click', (ev) => {
|
||||
const cell = ev.target.closest('[data-at]');
|
||||
if (cell) move(+cell.getAttribute('data-at'));
|
||||
});
|
||||
|
||||
view.addEventListener('keydown', (ev) => {
|
||||
const perScreen = Math.max(1, Math.floor(view.clientHeight / LH) - 1) * ROW;
|
||||
/* A null prototype: the key is `ev.key`, which this page does not choose, and an
|
||||
* inherited `constructor` would pass the `!= null` test below and move the caret by a
|
||||
* function — NaN, and a caret that cannot be moved back. */
|
||||
const keys = Object.assign(Object.create(null), {
|
||||
ArrowLeft: -1, ArrowRight: 1, ArrowUp: -ROW, ArrowDown: ROW,
|
||||
PageUp: -perScreen, PageDown: perScreen,
|
||||
});
|
||||
if (keys[ev.key] != null) { ev.preventDefault(); return move(caret + keys[ev.key]); }
|
||||
if (ev.key === 'Home') { ev.preventDefault(); return move(ev.ctrlKey ? 0 : caret - (caret % ROW)); }
|
||||
if (ev.key === 'End') { ev.preventDefault(); return move(ev.ctrlKey ? data.length - 1 : caret - (caret % ROW) + ROW - 1); }
|
||||
|
||||
/* TYPING IS NIBBLE BY NIBBLE, which is how every hex editor takes input: the first digit
|
||||
* replaces the high half and leaves the caret where it is, the second replaces the low
|
||||
* half and moves on. Anything else — a modifier, a letter past f — is not ours. */
|
||||
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
||||
const d = parseInt(ev.key, 16);
|
||||
if (ev.key.length !== 1 || isNaN(d)) return;
|
||||
ev.preventDefault();
|
||||
data[caret] = nibble
|
||||
? ((data[caret] & 0xf0) | d)
|
||||
: ((data[caret] & 0x0f) | (d << 4));
|
||||
const cell = layer.querySelector('[data-at="' + caret + '"]');
|
||||
if (cell) {
|
||||
cell.textContent = HEX[data[caret]];
|
||||
cell.classList.add('fsf-hex-edited');
|
||||
/* the text column of that line, rebuilt for the one character that changed */
|
||||
cell.closest('.fsf-hex-line').querySelector('.fsf-hex-text').textContent = textOf(data, caret - (caret % ROW));
|
||||
}
|
||||
if (nibble) { nibble = 0; if (caret + 1 < data.length) move(caret + 1); }
|
||||
else nibble = 1;
|
||||
});
|
||||
|
||||
fill(container, view);
|
||||
draw(true);
|
||||
view.focus();
|
||||
|
||||
return { value: () => data };
|
||||
},
|
||||
|
||||
/* base64 for the ubus `file write`, in chunks small enough for `String.fromCharCode` not to
|
||||
* blow the argument list. */
|
||||
base64(bytes, from, to) {
|
||||
let s = '';
|
||||
for (let i = from; i < to; i += 4096)
|
||||
s += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(to, i + 4096)));
|
||||
return btoa(s);
|
||||
},
|
||||
});
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jonas Pytte
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
"""
|
||||
This license applies to parts of the `package/src/prism` subdirectory originating
|
||||
from https://github.com/PrismJS/prism:
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2012 Lea Verou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
//#region src/utils/bracket.ts
|
||||
var testBracket = (str, brackets, l) => {
|
||||
return brackets.indexOf(str[0]) + 1 || l && brackets.indexOf(str[l]) + 1;
|
||||
};
|
||||
//#endregion
|
||||
export { testBracket as t };
|
||||
|
||||
//# sourceMappingURL=bracket-CStGDfC7.js.map
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
import { a as languages, i as highlightTokens, l as tokenizeText } from "./core-DEy9UQvI.js";
|
||||
//#region src/core.ts
|
||||
/**
|
||||
* Creates a code editor using the specified container and options.
|
||||
* @param container Element to append the editor to or a selector.
|
||||
* This can also be a `ShadowRoot` or `DocumentFragment` for example.
|
||||
* If omitted, you must manually append `editor.container` to the DOM.
|
||||
* @param options Options the editor is initialized with.
|
||||
* If omitted, the editor won't function until you call `editor.setOptions`.
|
||||
* @param extensions Extensions added before the first render. You can still add extensions later.
|
||||
* @returns Object to interact with the created editor.
|
||||
*/
|
||||
var createEditor = (container, options, ...extensions) => {
|
||||
let language;
|
||||
let prevLines = [];
|
||||
let activeLine;
|
||||
let value = "";
|
||||
let activeLineNumber;
|
||||
let focused = false;
|
||||
let handleSelectionChange = true;
|
||||
let tokens = [];
|
||||
let readOnly;
|
||||
let lineCount = 0;
|
||||
const scrollContainer = editorTemplate();
|
||||
const wrapper = scrollContainer.firstChild;
|
||||
const lines = wrapper.children;
|
||||
const overlays = lines[0];
|
||||
const textarea = overlays.firstChild;
|
||||
const currentOptions = {
|
||||
language: "text",
|
||||
value
|
||||
};
|
||||
const currentExtensions = new Set(extensions);
|
||||
const listeners = {};
|
||||
const setOptions = (options) => {
|
||||
Object.assign(currentOptions, options);
|
||||
let isNewVal = value != (value = options.value ?? value);
|
||||
let isNewLang = language != (language = currentOptions.language);
|
||||
readOnly = !!currentOptions.readOnly;
|
||||
scrollContainer.style.tabSize = currentOptions.tabSize || 2;
|
||||
textarea.inputMode = readOnly ? "none" : "";
|
||||
textarea.setAttribute("aria-readonly", readOnly);
|
||||
updateClassName();
|
||||
updateExtensions();
|
||||
if (isNewVal) {
|
||||
if (!focused) textarea.remove();
|
||||
textarea.value = value;
|
||||
textarea.selectionEnd = 0;
|
||||
if (!focused) overlays.prepend(textarea);
|
||||
}
|
||||
if (isNewVal || isNewLang) update();
|
||||
};
|
||||
const update = () => {
|
||||
tokens = tokenizeText(value = textarea.value, languages[language] || {});
|
||||
dispatchEvent("tokenize", tokens, language, value);
|
||||
let newLines = highlightTokens(tokens).split("\n");
|
||||
let start = 0;
|
||||
let end2 = lineCount;
|
||||
let end1 = lineCount = newLines.length;
|
||||
while (newLines[start] == prevLines[start] && start < end1) ++start;
|
||||
while (end1 && newLines[--end1] == prevLines[--end2]);
|
||||
if (start == end1 && start == end2) lines[start + 1].innerHTML = newLines[start] + "\n";
|
||||
else {
|
||||
let insertStart = end2 < start ? end2 : start - 1;
|
||||
let i = insertStart;
|
||||
let newHTML = "";
|
||||
while (i < end1) newHTML += `<div class=pce-line aria-hidden=true>${newLines[++i]}\n</div>`;
|
||||
for (i = end1 < start ? end1 : start - 1; i < end2; i++) lines[start + 1].remove();
|
||||
if (newHTML) lines[insertStart + 1].insertAdjacentHTML("afterend", newHTML);
|
||||
scrollContainer.style.setProperty("--number-width", (0 | Math.log10(lineCount)) + 1 + ".001ch");
|
||||
}
|
||||
dispatchEvent("update", value);
|
||||
dispatchSelection(true);
|
||||
if (handleSelectionChange) setTimeout(setTimeout, 0, () => handleSelectionChange = true);
|
||||
prevLines = newLines;
|
||||
handleSelectionChange = false;
|
||||
};
|
||||
const updateExtensions = (newExtensions) => {
|
||||
(newExtensions || currentExtensions).forEach((extension) => {
|
||||
if (typeof extension == "object") {
|
||||
extension.update(self, currentOptions);
|
||||
if (newExtensions) currentExtensions.add(extension);
|
||||
} else {
|
||||
extension(self, currentOptions);
|
||||
if (!newExtensions) currentExtensions.delete(extension);
|
||||
}
|
||||
});
|
||||
};
|
||||
const updateClassName = ([start, end] = getInputSelection()) => {
|
||||
scrollContainer.className = `prism-code-editor language-${language}${currentOptions.lineNumbers == false ? "" : " show-line-numbers"} pce-${currentOptions.wordWrap ? "" : "no"}wrap${currentOptions.rtl ? " pce-rtl" : ""} pce-${start < end ? "has" : "no"}-selection${focused ? " pce-focus" : ""}${readOnly ? " pce-readonly" : ""}${currentOptions.class ? " " + currentOptions.class : ""}`;
|
||||
};
|
||||
const getInputSelection = () => [
|
||||
textarea.selectionStart,
|
||||
textarea.selectionEnd,
|
||||
textarea.selectionDirection
|
||||
];
|
||||
const keyCommandMap = { Escape() {
|
||||
textarea.blur();
|
||||
} };
|
||||
const inputCommandMap = {};
|
||||
const dispatchEvent = (name, ...args) => {
|
||||
listeners[name]?.forEach((handler) => handler.apply(self, args));
|
||||
currentOptions["on" + name[0].toUpperCase() + name.slice(1)]?.(...args, self);
|
||||
};
|
||||
const dispatchSelection = (force) => {
|
||||
if (force || handleSelectionChange) {
|
||||
const selection = getInputSelection();
|
||||
const newLine = lines[activeLineNumber = numLines(value, 0, selection[selection[2] < "f" ? 0 : 1])];
|
||||
if (newLine != activeLine) {
|
||||
activeLine?.classList.remove("active-line");
|
||||
newLine.classList.add("active-line");
|
||||
activeLine = newLine;
|
||||
}
|
||||
updateClassName(selection);
|
||||
dispatchEvent("selectionChange", selection, value);
|
||||
}
|
||||
};
|
||||
const self = {
|
||||
container: scrollContainer,
|
||||
wrapper,
|
||||
lines,
|
||||
textarea,
|
||||
get activeLine() {
|
||||
return activeLineNumber;
|
||||
},
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
options: currentOptions,
|
||||
get focused() {
|
||||
return focused;
|
||||
},
|
||||
get tokens() {
|
||||
return tokens;
|
||||
},
|
||||
inputCommandMap,
|
||||
keyCommandMap,
|
||||
extensions: {},
|
||||
setOptions,
|
||||
update,
|
||||
getSelection: getInputSelection,
|
||||
addExtensions(...extensions) {
|
||||
updateExtensions(extensions);
|
||||
},
|
||||
on: (name, handler) => {
|
||||
(listeners[name] ||= /* @__PURE__ */ new Set()).add(handler);
|
||||
return () => listeners[name].delete(handler);
|
||||
},
|
||||
remove() {
|
||||
scrollContainer.remove();
|
||||
}
|
||||
};
|
||||
addListener(textarea, "keydown", (e) => {
|
||||
keyCommandMap[e.key]?.(e, getInputSelection(), value) && preventDefault(e);
|
||||
});
|
||||
addListener(textarea, "beforeinput", (e) => {
|
||||
if (readOnly || e.inputType == "insertText" && inputCommandMap[e.data]?.(e, getInputSelection(), value)) preventDefault(e);
|
||||
});
|
||||
addListener(textarea, "input", update);
|
||||
addListener(textarea, "blur", () => {
|
||||
selectionChange = null;
|
||||
focused = false;
|
||||
updateClassName();
|
||||
});
|
||||
addListener(textarea, "focus", () => {
|
||||
selectionChange = dispatchSelection;
|
||||
focused = true;
|
||||
updateClassName();
|
||||
});
|
||||
addListener(textarea, "selectionchange", (e) => {
|
||||
dispatchSelection(!e.isTrusted);
|
||||
preventDefault(e);
|
||||
});
|
||||
getElement(container)?.append(scrollContainer);
|
||||
options && setOptions(options);
|
||||
return self;
|
||||
};
|
||||
/**
|
||||
* Almost identical to {@link createEditor}, but instead of appending the editor to your
|
||||
* element, the editor replaces it.
|
||||
*
|
||||
* The `textContent` of the placeholder will be the code in the editor unless `options.value` is defined.
|
||||
* @param placeholder Node or selector which will be replaced by the editor.
|
||||
* @param options Options the editor is initialized with.
|
||||
* @param extensions Extensions added before the first render. You can still add extensions later.
|
||||
* @returns Object to interact with the created editor.
|
||||
*/
|
||||
var editorFromPlaceholder = (placeholder, options, ...extensions) => {
|
||||
const el = getElement(placeholder);
|
||||
const editor = createEditor(null, Object.assign({ value: el.textContent }, options), ...extensions);
|
||||
el.replaceWith(editor.container);
|
||||
return editor;
|
||||
};
|
||||
/** Equivalent to `document` in a browser setting, `null` otherwise. */
|
||||
var doc = "u" > typeof window ? document : null;
|
||||
var templateEl = /* @__PURE__ */ doc?.createElement("div");
|
||||
var createTemplate = (html, node) => {
|
||||
if (templateEl) {
|
||||
templateEl.innerHTML = html;
|
||||
node = templateEl.firstChild;
|
||||
}
|
||||
return () => node.cloneNode(true);
|
||||
};
|
||||
var addListener = (target, type, listener, options) => target.addEventListener(type, listener, options);
|
||||
var getElement = (el) => typeof el == "string" ? doc.querySelector(el) : el;
|
||||
/**
|
||||
* Counts number of lines in the string between `start` and `end`.
|
||||
* If start and end are omitted, the whole string is searched.
|
||||
*/
|
||||
var numLines = (str, start = 0, end = Infinity) => {
|
||||
let count = 1;
|
||||
for (; (start = str.indexOf("\n", start) + 1) && start <= end; count++);
|
||||
return count;
|
||||
};
|
||||
/** Object storing all language specific behavior. */
|
||||
var languageMap = {};
|
||||
var editorTemplate = /* @__PURE__ */ createTemplate("<div><div class=pce-wrapper><div class=pce-overlays><textarea class=pce-textarea spellcheck=false autocapitalize=off autocomplete=off>");
|
||||
var preventDefault = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
var setSelectionChange = (f) => selectionChange = f;
|
||||
var selectionChange;
|
||||
if (doc) addListener(doc, "selectionchange", () => selectionChange?.());
|
||||
//#endregion
|
||||
export { editorFromPlaceholder as a, numLines as c, doc as i, preventDefault as l, createEditor as n, getElement as o, createTemplate as r, languageMap as s, addListener as t, setSelectionChange as u };
|
||||
|
||||
//# sourceMappingURL=core-C0HOUBwg.js.map
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
//#region src/prism/core.js
|
||||
var plainTextGrammar = {};
|
||||
var rest = Symbol();
|
||||
var tokenize = Symbol();
|
||||
/** @param {*} id */
|
||||
var resolve = (id) => typeof id == "string" ? languages[id] : id;
|
||||
/** @type {Record<string, any>} */
|
||||
var languages = {
|
||||
plain: plainTextGrammar,
|
||||
plaintext: plainTextGrammar,
|
||||
text: plainTextGrammar,
|
||||
txt: plainTextGrammar
|
||||
};
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {any} grammar
|
||||
*/
|
||||
var tokenizeText = (text, grammar) => (grammar[tokenize] || withoutTokenizer)(text, grammar);
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {any} grammar
|
||||
*/
|
||||
var withoutTokenizer = (text, grammar) => {
|
||||
/** @type {LinkedListNode} */
|
||||
var startNode = [text];
|
||||
var restGrammar;
|
||||
/** @type {(string | Token)[]} */
|
||||
var array = [], i = 0;
|
||||
while (restGrammar = resolve(grammar[rest])) {
|
||||
delete grammar[rest];
|
||||
Object.assign(grammar, restGrammar);
|
||||
}
|
||||
matchGrammar(text, grammar, startNode, 0);
|
||||
while (array[i++] = startNode[0], startNode = startNode[1]);
|
||||
return array;
|
||||
};
|
||||
/**
|
||||
* @param {string} string
|
||||
* @param {RegExp} pattern
|
||||
* @param {string} replacement
|
||||
*/
|
||||
var escapeHtml = (string, pattern, replacement) => {
|
||||
return string.replace(/&/g, "&").replace(pattern, replacement);
|
||||
};
|
||||
var closingTag = "</span>";
|
||||
var openingTags = "";
|
||||
var closingTags = "";
|
||||
/** @param {(string | Token)[]} tokens */
|
||||
var highlightTokens = (tokens) => {
|
||||
var str = "", token, i = 0;
|
||||
while (token = tokens[i++]) str += stringify(token);
|
||||
return str;
|
||||
};
|
||||
/**
|
||||
* @param {string | Token | (string | Token)[]} token
|
||||
* @returns {string}
|
||||
*/
|
||||
var stringify = (token) => {
|
||||
if (token instanceof Token) {
|
||||
var { type, alias, content } = token;
|
||||
var prevOpening = openingTags;
|
||||
var prevClosing = closingTags;
|
||||
var opening = `<span class="token ${type + (alias ? " " + alias : "") + (type == "keyword" && typeof content == "string" ? " keyword-" + content.replace(/"|\s/g, "") : "")}">`;
|
||||
closingTags += closingTag;
|
||||
openingTags += opening;
|
||||
var contentStr = stringify(content);
|
||||
openingTags = prevOpening;
|
||||
closingTags = prevClosing;
|
||||
return opening + contentStr + closingTag;
|
||||
}
|
||||
if (typeof token != "string") return highlightTokens(token);
|
||||
token = escapeHtml(token, /</g, "<");
|
||||
if (closingTags && token.includes("\n")) return token.replace(/\n/g, closingTags + "\n" + openingTags);
|
||||
return token;
|
||||
};
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {*} ref
|
||||
*/
|
||||
var highlightText = (text, ref) => highlightTokens(tokenizeText(text, resolve(ref)));
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {any} grammar
|
||||
* @param {LinkedListNode} startNode
|
||||
* @param {number} startPos
|
||||
* @param {[string, number, number]} [rematch]
|
||||
*
|
||||
* @typedef {[string | Token, LinkedListNode?]} LinkedListNode
|
||||
*/
|
||||
var matchGrammar = (text, grammar, startNode, startPos, rematch) => {
|
||||
for (var token in grammar) if (grammar[token]) for (var j = 0, p = grammar[token], patternObj, patterns = Array.isArray(p) ? p : [p]; patternObj = patterns[j]; j++) {
|
||||
if (rematch && rematch[0] == token && rematch[1] == j) return;
|
||||
/** @type {RegExp} */
|
||||
var pattern = patternObj.pattern || patternObj;
|
||||
var inside = resolve(patternObj.inside);
|
||||
var lookbehind = patternObj.lookbehind;
|
||||
var greedy = pattern.global;
|
||||
var alias = patternObj.alias;
|
||||
for (var currentNode = startNode, pos = startPos; currentNode && (!rematch || pos < rematch[2]); pos += currentNode[0].length, currentNode = currentNode[1]) {
|
||||
var str = currentNode[0];
|
||||
var removeCount = 0;
|
||||
var match;
|
||||
if (str instanceof Token) continue;
|
||||
pattern.lastIndex = greedy ? pos : 0;
|
||||
match = pattern.exec(greedy ? text : str);
|
||||
if (!match && greedy) break;
|
||||
if (!(match && match[0])) continue;
|
||||
var lookbehindLength = lookbehind && match[1] ? match[1].length : 0;
|
||||
var from = match.index + lookbehindLength;
|
||||
var matchStr = match[0].slice(lookbehindLength);
|
||||
var to = from + matchStr.length;
|
||||
var k, p;
|
||||
if (greedy) {
|
||||
for (; p = pos + currentNode[0].length, from >= p; currentNode = currentNode[1], pos = p);
|
||||
if (currentNode[0] instanceof Token) continue;
|
||||
for (k = currentNode, p = pos; (p += k[0].length) < to; k = k[1], removeCount++);
|
||||
str = text.slice(pos, p);
|
||||
from -= pos;
|
||||
to -= pos;
|
||||
}
|
||||
var after = str.slice(to);
|
||||
var reach = pos + str.length;
|
||||
var newToken = new Token(token, inside ? tokenizeText(matchStr, inside) : matchStr, matchStr, alias);
|
||||
var next = currentNode, i = 0;
|
||||
var nestedRematch;
|
||||
while (next = next[1], i++ < removeCount);
|
||||
if (after) if (!next || next[0] instanceof Token) next = [after, next];
|
||||
else next[0] = after + next[0];
|
||||
pos += from;
|
||||
currentNode[0] = from ? str.slice(0, from) : newToken;
|
||||
if (from) currentNode = currentNode[1] = [newToken, next];
|
||||
else currentNode[1] = next;
|
||||
if (removeCount) {
|
||||
matchGrammar(text, grammar, currentNode, pos, nestedRematch = [
|
||||
token,
|
||||
j,
|
||||
reach
|
||||
]);
|
||||
reach = nestedRematch[2];
|
||||
}
|
||||
if (rematch && reach > rematch[2]) rematch[2] = reach;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Creates a new token.
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string | any[]} content
|
||||
* @param {string} matchedStr
|
||||
* @param {string} alias
|
||||
* @class
|
||||
*/
|
||||
function Token(type, content, matchedStr, alias) {
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
this.alias = alias;
|
||||
this.length = matchedStr.length;
|
||||
}
|
||||
//#endregion
|
||||
export { languages as a, tokenize as c, highlightTokens as i, tokenizeText as l, escapeHtml as n, resolve as o, highlightText as r, rest as s, Token as t, withoutTokenizer as u };
|
||||
|
||||
//# sourceMappingURL=core-DEy9UQvI.js.map
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { t as testBracket } from "../../bracket-CStGDfC7.js";
|
||||
//#region src/extensions/matchBrackets/index.ts
|
||||
/**
|
||||
* Extension that matches punctuation tokens together. Intended for matching brackets.
|
||||
*
|
||||
* Without rainbow brackets, this extension can be added dynamically with no downsides.
|
||||
*
|
||||
* @param rainbowBrackets Whether to add extra classes to brackets for styling. Defaults
|
||||
* to `true`. When enabled, adding the extension dynamically will force a rerender to add
|
||||
* these extra classes.
|
||||
* @param pairs Which characters to match together. The opening character must be followed
|
||||
* by the corresponding closing character. Defaults to `"()[]{}"`.
|
||||
*/
|
||||
var matchBrackets = (rainbowBrackets = true, pairs = "()[]{}") => {
|
||||
let bracketIndex;
|
||||
let sp;
|
||||
const stack = [];
|
||||
const self = (editor) => {
|
||||
editor.extensions.matchBrackets = self;
|
||||
editor.on("tokenize", matchBrackets);
|
||||
if (rainbowBrackets && editor.tokens[0]) editor.update();
|
||||
else matchBrackets(editor.tokens);
|
||||
};
|
||||
const brackets = self.brackets = [];
|
||||
const pairMap = self.pairs = [];
|
||||
const matchBrackets = (tokens) => {
|
||||
pairMap.length = brackets.length = sp = bracketIndex = 0;
|
||||
matchRecursive(tokens, 0);
|
||||
if (rainbowBrackets) for (let i = 0, bracket; bracket = brackets[i];) {
|
||||
let alias = bracket[0].alias;
|
||||
bracket[0].alias = (alias ? alias + " " : "") + `bracket-${i++ in pairMap ? "level-" + bracket[3] % 12 : "error"}`;
|
||||
}
|
||||
};
|
||||
const matchRecursive = (tokens, position) => {
|
||||
let token;
|
||||
let i = 0;
|
||||
for (; token = tokens[i++];) {
|
||||
let length = token.length;
|
||||
if (typeof token != "string") {
|
||||
let content = token.content;
|
||||
if (Array.isArray(content)) matchRecursive(content, position);
|
||||
else if ((token.alias || token.type) == "punctuation") {
|
||||
let bracketType = testBracket(content, pairs, length - 1);
|
||||
let isOpening = bracketType % 2;
|
||||
if (bracketType) {
|
||||
brackets[bracketIndex] = [
|
||||
token,
|
||||
position,
|
||||
position + length,
|
||||
sp,
|
||||
content,
|
||||
!!isOpening
|
||||
];
|
||||
if (isOpening) stack[sp++] = [bracketIndex, bracketType + 1];
|
||||
else for (let i = sp, entry; entry = stack[--i];) if (bracketType == entry[1]) {
|
||||
pairMap[pairMap[bracketIndex] = entry[0]] = bracketIndex;
|
||||
brackets[bracketIndex][3] = sp = i;
|
||||
break;
|
||||
}
|
||||
bracketIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
position += length;
|
||||
}
|
||||
};
|
||||
return self;
|
||||
};
|
||||
//#endregion
|
||||
export { matchBrackets };
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
import { c as numLines, i as doc, l as preventDefault, r as createTemplate, t as addListener } from "../../core-C0HOUBwg.js";
|
||||
import { _ as getLineStart, a as getLineBefore, d as isWebKit, g as getLineEnd, p as regexEscape, s as getModifierCode, t as addOverlay, u as isMac, x as updateNode, y as getStyleValue } from "../../utils-Cmdnutew.js";
|
||||
import { s as mod } from "../../utils-ChVWFtO9.js";
|
||||
import { t as createSearchAPI } from "../../search-CvZenEEf.js";
|
||||
import { t as createReplaceAPI } from "../../replace-BGouct-V.js";
|
||||
import { n as highlightSelectionMatches, t as highlightCurrentWord } from "../../selection-ClP9zgI_.js";
|
||||
//#region src/extensions/search/widget.ts
|
||||
var shortcut = ` (Alt+${isMac ? "Cmd+" : ""}`;
|
||||
var template = /* @__PURE__ */ createTemplate(`<div class=prism-search-container style=display:flex;align-items:flex-start;justify-content:flex-end><div dir=ltr class=prism-search><button type=button aria-expanded=false title="Toggle Replace" class=pce-expand></button><div spellcheck=false><div><div class="pce-input pce-find"><input autocorrect=off autocapitalize=off placeholder=Find aria-label=Find><button type=button class=pce-prev title="Previous Match (Shift+Enter)"></button><button type=button class=pce-next title="Next Match (Enter)"></button><div class=search-error></div></div><button type=button class=pce-close title="Close (Esc)"></button></div><div class="pce-input pce-replace"><input autocorrect=off autocapitalize=off placeholder=Replace aria-label=Replace><button type=button title=(Enter)>Replace</button><button type=button title=(${isMac ? "Cmd" : "Ctrl+Alt"}+Enter)>All</button></div><div class=pce-options><div class=pce-match-count>0<span> of </span>0</div><button type=button aria-pressed=false class=pce-regex title="RegExp Search${shortcut}R)"><span aria-hidden=true></span></button><button type=button aria-pressed=false title="Preserve Case${shortcut}P)"><span aria-hidden=true>Aa</span></button><button type=button aria-pressed=false class=pce-whole title="Match Whole Word${shortcut}W)"><span aria-hidden=true>ab</span></button><button type=button aria-pressed=false class=pce-in-selection title="Find in Selection${shortcut}L)">`);
|
||||
var toggleAttr = (el, name) => el.setAttribute(name, el.getAttribute(name) == "false");
|
||||
/**
|
||||
* Extension that adds a widget for search and replace functionality.
|
||||
* This extension needs styles from `prism-code-editor/search.css`.
|
||||
*
|
||||
* Once added to an editor the widget can be opened/closed programmatically with the
|
||||
* `editor.extensions.searchWidget` object.
|
||||
*/
|
||||
var searchWidget = () => {
|
||||
let prevLength;
|
||||
let useRegExp;
|
||||
let matchCase;
|
||||
let wholeWord;
|
||||
let searchSelection;
|
||||
let isOpen;
|
||||
let currentSelection;
|
||||
let prevUserSelection;
|
||||
let prevMargin;
|
||||
let selectNext = false;
|
||||
let marginTop;
|
||||
const self = (editor) => {
|
||||
editor.extensions.searchWidget = self;
|
||||
const { textarea, wrapper, container, getSelection } = editor;
|
||||
const replaceAPI = createReplaceAPI(editor);
|
||||
const startSearch = (selectMatch) => {
|
||||
if (selectMatch && !isWebKit) textarea.setSelectionRange(...prevUserSelection);
|
||||
const error = replaceAPI.search(findInput.value, matchCase, wholeWord, useRegExp, searchSelection);
|
||||
const index = error ? -1 : selectNext ? replaceAPI.next() : replaceAPI.closest();
|
||||
updateNode(current, index + 1);
|
||||
updateNode(total, replaceAPI.matches.length);
|
||||
findContainer.classList.toggle("pce-error", !!error);
|
||||
if (error) errorEl.textContent = error;
|
||||
else if (selectMatch || selectNext) replaceAPI.selectMatch(index, prevMargin);
|
||||
};
|
||||
const open = (focusInput = true) => {
|
||||
if (!isOpen) {
|
||||
isOpen = true;
|
||||
if (marginTop == null) prevMargin = marginTop = getStyleValue(wrapper, "marginTop");
|
||||
prevUserSelection = getSelection();
|
||||
addOverlay(editor, searchContainer);
|
||||
updateMargin();
|
||||
resize();
|
||||
observer?.observe(container);
|
||||
}
|
||||
if (focusInput) findInput.select();
|
||||
};
|
||||
const close = self.close = (focusTextarea = true) => {
|
||||
if (isOpen) {
|
||||
isOpen = false;
|
||||
replaceAPI.stopSearch();
|
||||
searchContainer.remove();
|
||||
updateMargin();
|
||||
observer?.disconnect();
|
||||
focusTextarea && textarea.focus();
|
||||
}
|
||||
};
|
||||
const move = (next) => {
|
||||
if (replaceAPI.matches[0]) {
|
||||
const index = replaceAPI[next ? "next" : "prev"]();
|
||||
replaceAPI.selectMatch(index, prevMargin);
|
||||
updateNode(current, index + 1);
|
||||
}
|
||||
};
|
||||
const updateMargin = () => {
|
||||
const newMargin = isOpen ? getStyleValue(search, "top") + getStyleValue(search, "height") : marginTop;
|
||||
const newScroll = container.scrollTop + newMargin - prevMargin;
|
||||
wrapper.style.marginTop = isOpen ? newMargin + "px" : "";
|
||||
container.scrollTop = newScroll;
|
||||
prevMargin = newMargin;
|
||||
};
|
||||
const resize = () => div.style.setProperty("--search-width", `min(${container.clientWidth - 2}px - 2.4em - var(--padding-left),20em)`);
|
||||
const observer = window.ResizeObserver && new ResizeObserver(resize);
|
||||
const replace = () => {
|
||||
selectNext = true;
|
||||
const index = replaceAPI.replace(replaceInput.value);
|
||||
if (index != null) {
|
||||
updateNode(current, index + 1);
|
||||
replaceAPI.selectMatch(index, prevMargin);
|
||||
}
|
||||
selectNext = false;
|
||||
};
|
||||
const replaceAll = () => {
|
||||
replaceAPI.replaceAll(replaceInput.value);
|
||||
};
|
||||
const keyCodeButtonMap = {
|
||||
80: matchCaseEl,
|
||||
87: wholeWordEl,
|
||||
82: useRegExpEl,
|
||||
76: inSelectionEl
|
||||
};
|
||||
const elementHandlerMap = /* @__PURE__ */ new Map([
|
||||
[nextEl, () => move(true)],
|
||||
[prevEl, move],
|
||||
[closeEl, close],
|
||||
[replaceEl, replace],
|
||||
[replaceAllEl, replaceAll],
|
||||
[toggle, () => {
|
||||
toggleAttr(toggle, "aria-expanded");
|
||||
updateMargin();
|
||||
}],
|
||||
[matchCaseEl, () => matchCase = !matchCase],
|
||||
[useRegExpEl, () => useRegExp = !useRegExp],
|
||||
[wholeWordEl, () => wholeWord = !wholeWord],
|
||||
[inSelectionEl, () => {
|
||||
const value = editor.value;
|
||||
if (searchSelection) searchSelection = void 0;
|
||||
else {
|
||||
searchSelection = getSelection().slice(0, 2);
|
||||
if (numLines(value, ...searchSelection) > 1) searchSelection = [getLineStart(value, searchSelection[0]), getLineEnd(value, searchSelection[1])];
|
||||
}
|
||||
prevLength = value.length;
|
||||
}]
|
||||
]);
|
||||
const getSelectedWord = (selectionOnly) => {
|
||||
let [start, end] = getSelection();
|
||||
let value = editor.value;
|
||||
let word = value.slice(start, end) || (selectionOnly ? "" : /[_\p{N}\p{L}]*$/u.exec(getLineBefore(value, start))[0] + /^[_\p{N}\p{L}]*/u.exec(value.slice(start))[0]);
|
||||
return word.includes("\n") ? "" : useRegExp ? regexEscape(word) : word;
|
||||
};
|
||||
addListener(wrapper, "keydown", (e) => {
|
||||
const code = getModifierCode(e);
|
||||
const keyCode = e.keyCode;
|
||||
const isF3 = keyCode == 114;
|
||||
if (keyCode == 70 && code == mod) {
|
||||
preventDefault(e);
|
||||
open();
|
||||
let word = getSelectedWord();
|
||||
if (word) {
|
||||
doc.execCommand("insertText", false, word);
|
||||
findInput.select();
|
||||
} else startSearch();
|
||||
} else if ((isF3 || keyCode == 71) && (code & 7) == (isF3 ? 0 : mod)) {
|
||||
preventDefault(e);
|
||||
open(false);
|
||||
findInput.value ||= getSelectedWord();
|
||||
startSearch();
|
||||
move(code < 8);
|
||||
} else if (isMac ? code == 5 && keyCode == 70 : code == 2 && keyCode == 72) {
|
||||
const searchTerm = getSelectedWord(true);
|
||||
const target = e.target;
|
||||
const shouldSearch = searchTerm && target != findInput && target != replaceInput;
|
||||
const newTarget = target == findInput || shouldSearch ? replaceInput : findInput;
|
||||
preventDefault(e);
|
||||
open(false);
|
||||
if (toggle.getAttribute("aria-expanded") != "true") toggle.click();
|
||||
if (shouldSearch) {
|
||||
findInput.value = searchTerm;
|
||||
startSearch();
|
||||
}
|
||||
newTarget.focus();
|
||||
newTarget.select();
|
||||
}
|
||||
});
|
||||
addListener(textarea, "beforeinput", () => {
|
||||
if (isOpen && searchSelection) currentSelection = getSelection();
|
||||
});
|
||||
editor.on("update", () => {
|
||||
if (!isOpen) return;
|
||||
if (searchSelection && currentSelection) {
|
||||
const diff = prevLength - (prevLength = editor.value.length);
|
||||
const end = currentSelection[1];
|
||||
if (end <= searchSelection[1]) {
|
||||
searchSelection[1] -= diff;
|
||||
if (end <= searchSelection[0] - +(diff < 0)) searchSelection[0] -= diff;
|
||||
}
|
||||
}
|
||||
startSearch();
|
||||
});
|
||||
editor.on("selectionChange", (selection) => {
|
||||
if (isOpen && editor.focused) prevUserSelection = selection;
|
||||
});
|
||||
addListener(searchContainer, "click", (e) => {
|
||||
const target = e.target;
|
||||
const remove = editor.on("update", () => target.focus());
|
||||
elementHandlerMap.get(target)?.();
|
||||
if (target.matches(".pce-options>button")) {
|
||||
toggleAttr(target, "aria-pressed");
|
||||
startSearch(true);
|
||||
}
|
||||
remove();
|
||||
});
|
||||
addListener(findInput, "input", () => isOpen && startSearch(true));
|
||||
addListener(searchContainer, "keydown", (e) => {
|
||||
const shortcut = getModifierCode(e);
|
||||
const target = e.target;
|
||||
const keyCode = e.keyCode;
|
||||
const isFind = target == findInput;
|
||||
if (shortcut == (isMac ? 5 : 1)) {
|
||||
if (keyCodeButtonMap[keyCode]) {
|
||||
preventDefault(e);
|
||||
keyCodeButtonMap[keyCode].click();
|
||||
}
|
||||
} else if (keyCode == 13 && target.tagName == "INPUT") {
|
||||
preventDefault(e);
|
||||
if (!shortcut) isFind ? move(true) : replaceEl.click();
|
||||
else if (shortcut == 8 && isFind) move();
|
||||
else if (shortcut == (isMac ? 4 : 3) && !isFind) replaceAllEl.click();
|
||||
target.focus();
|
||||
} else if (!shortcut && keyCode == 27) close();
|
||||
});
|
||||
self.open = (focusInput) => {
|
||||
open(focusInput);
|
||||
startSearch();
|
||||
};
|
||||
replaceAPI.container.className = "pce-matches";
|
||||
};
|
||||
const searchContainer = template();
|
||||
const search = self.element = searchContainer.firstChild;
|
||||
const [toggle, div] = search.children;
|
||||
const rows = div.children;
|
||||
const [findContainer, closeEl] = rows[0].children;
|
||||
const [findInput, prevEl, nextEl, errorEl] = findContainer.children;
|
||||
const [replaceInput, replaceEl, replaceAllEl] = rows[1].children;
|
||||
const [matchCount, useRegExpEl, matchCaseEl, wholeWordEl, inSelectionEl] = rows[2].children;
|
||||
const [current, , total] = matchCount.childNodes;
|
||||
self.open = self.close = () => {};
|
||||
return self;
|
||||
};
|
||||
//#endregion
|
||||
//#region src/extensions/search/invisibles.ts
|
||||
/**
|
||||
* Extension that highlights selected tabs and spaces as an overlay. To instead highlight
|
||||
* all spaces and tabs, use {@link tokenizeInvisibles}.
|
||||
*
|
||||
* Requires styling from `prism-code-editor/invisibles.css`.
|
||||
*/
|
||||
var showInvisibles = () => {
|
||||
return (editor) => {
|
||||
const searchAPI = createSearchAPI(editor);
|
||||
const matches = searchAPI.matches;
|
||||
const container = searchAPI.container;
|
||||
const nodes = container.children;
|
||||
const tabs = [];
|
||||
const update = () => {
|
||||
const value = editor.value;
|
||||
const [start, end] = editor.getSelection();
|
||||
searchAPI.search(" | ", true, false, true, [start, end]);
|
||||
for (let i = 0, l = matches.length; i < l; i++) if (value[matches[i][0]] == " " == !tabs[i]) nodes[i].className = (tabs[i] = !tabs[i]) ? "pce-tab" : "";
|
||||
};
|
||||
container.className = "pce-invisibles";
|
||||
if (editor.value) update();
|
||||
editor.on("selectionChange", update);
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
export { highlightCurrentWord, highlightSelectionMatches, searchWidget, showInvisibles };
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { a as editorFromPlaceholder, c as numLines, n as createEditor, s as languageMap } from "./core-C0HOUBwg.js";
|
||||
export { createEditor, editorFromPlaceholder, languageMap, numLines };
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
.prism-code-editor{overflow:auto;display:grid;background:var(--pce-bg);line-height:1.4;--_pse: var(--padding-inline, .75em);--_ns: var(--number-spacing, .75em);--padding-left: var(--_pse);--_sp: 0px;scroll-padding:var(--_sp);-webkit-user-select:none;user-select:none;isolation:isolate;white-space:pre}.show-line-numbers{--padding-left: calc(var(--_pse) + var(--number-width) + var(--_ns));scroll-padding-left:calc(var(--padding-left) + var(--_sp));grid:1fr / 0 1fr;counter-reset:line}.pce-focus{--_sp: var(--pce-scroll-padding, 2ch)}.pce-wrapper{margin:.5em 0;position:relative;pointer-events:none;-webkit-text-size-adjust:none;text-size-adjust:none}.pce-textarea{all:unset;box-sizing:border-box;height:100%;width:100%;color:#0000;-webkit-user-select:auto;user-select:auto;overflow:hidden;pointer-events:auto;caret-color:var(--pce-cursor)}.pce-textarea::selection{color:#0000}.pce-wrapper ::selection{background:var(--pce-selection)}.pce-no-selection textarea:focus{z-index:1}.pce-line,.pce-textarea{padding:0 var(--_pse) 0 var(--padding-left);position:relative}.show-line-numbers .pce-line:before{counter-increment:line;content:counter(line);display:inline-block;margin:0 0 0 calc(-1 * var(--padding-left));padding:0 var(--_ns) 0 0;box-sizing:border-box;color:var(--pce-line-number);text-align:end}.show-line-numbers:before{content:"";background:inherit;pointer-events:none}.show-line-numbers:before,.pce-line:before{position:sticky;height:100%;z-index:2;left:0;width:var(--padding-left)}.pce-wrap .pce-line:before{position:absolute;margin:0}.pce-overlays,.pce-overlays>*,pre.pce-guides .pce-line:after,.pce-no-selection .active-line:after,.active-line.match-highlight:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0}.show-line-numbers .pce-line:after{inset-inline-start:var(--padding-left)}.active-line:after{border:var(--pce-border-highlight);background:var(--pce-bg-highlight);z-index:-2}.pce-wrap{white-space:pre-wrap;word-break:break-word}.selection-matches span{background:var(--pce-selection-match)}.pce-nowrap .active-bracket{display:inline-block}
|
||||
luci-app-footstrap-files/htdocs/luci-static/resources/view/footstrap-files/vendor/pce/prism/index.js
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { a as languages, c as tokenize, i as highlightTokens, l as tokenizeText, r as highlightText, s as rest, t as Token, u as withoutTokenizer } from "../core-DEy9UQvI.js";
|
||||
export { Token, highlightText, highlightTokens, languages, rest, tokenize, tokenizeText, withoutTokenizer };
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { b as scrollToEl, c as insertText, h as addTextareaListener, m as setSelection } from "./utils-Cmdnutew.js";
|
||||
import { t as createSearchAPI } from "./search-CvZenEEf.js";
|
||||
//#region src/extensions/search/replace.ts
|
||||
/** Function adding both search and replace functionality to an editor. */
|
||||
var createReplaceAPI = (editor) => {
|
||||
const getSelection = editor.getSelection;
|
||||
const search = createSearchAPI(editor);
|
||||
const container = search.container;
|
||||
const matches = search.matches;
|
||||
const closest = () => {
|
||||
const caretPos = getSelection()[0];
|
||||
const l = matches.length;
|
||||
for (let i = l; i;) if (caretPos >= matches[--i][1]) return (i + (matches[i][0] < caretPos)) % l;
|
||||
return l ? 0 : -1;
|
||||
};
|
||||
const toggleClasses = () => {
|
||||
currentLine?.classList.toggle("match-highlight");
|
||||
currentMatch?.classList.toggle("pce-match");
|
||||
};
|
||||
const removeSelection = () => {
|
||||
if (hasSelected) {
|
||||
toggleClasses();
|
||||
hasSelected = false;
|
||||
}
|
||||
};
|
||||
const removeHandler = addTextareaListener(editor, "focus", removeSelection);
|
||||
let currentLine;
|
||||
let currentMatch;
|
||||
let hasSelected = false;
|
||||
return Object.assign(search, {
|
||||
next() {
|
||||
const cursor = getSelection()[1];
|
||||
const l = matches.length;
|
||||
for (let i = 0, match; i < l; i++) {
|
||||
match = matches[i];
|
||||
if (match[0] - (match[0] == match[1]) >= cursor) return i;
|
||||
}
|
||||
return l ? 0 : -1;
|
||||
},
|
||||
prev() {
|
||||
const cursor = getSelection()[0];
|
||||
const l = matches.length;
|
||||
for (let i = l, match; i;) {
|
||||
match = matches[--i];
|
||||
if (match[1] + (match[0] == match[1]) <= cursor) return i;
|
||||
}
|
||||
return l - 1;
|
||||
},
|
||||
closest,
|
||||
selectMatch(index, scrollPadding) {
|
||||
removeSelection();
|
||||
if (matches[index]) {
|
||||
setSelection(editor, ...matches[index]);
|
||||
currentLine = editor.lines[editor.activeLine];
|
||||
currentMatch = container.children[index];
|
||||
hasSelected = true;
|
||||
toggleClasses();
|
||||
if (currentMatch) scrollToEl(editor, currentMatch, scrollPadding);
|
||||
}
|
||||
},
|
||||
replace(str) {
|
||||
if (matches[0]) {
|
||||
let index = closest();
|
||||
let [start, end] = matches[index];
|
||||
let [caretStart, caretEnd] = getSelection();
|
||||
if (start != caretStart || end != caretEnd) return index;
|
||||
if (editor.value.slice(start, end) == str) return matches[++index] ? index : 0;
|
||||
return insertText(editor, str);
|
||||
}
|
||||
},
|
||||
replaceAll(str) {
|
||||
if (!matches[0]) return;
|
||||
let value = editor.value;
|
||||
let [start, end] = getSelection();
|
||||
let newLen = str.length;
|
||||
let newStart = start;
|
||||
let newEnd = end;
|
||||
let newValue = "";
|
||||
let l = matches.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const [matchStart, matchEnd] = matches[i];
|
||||
const lengthDiff = newLen - matchEnd + matchStart;
|
||||
const move = (pos) => matchStart > pos ? 0 : pos >= matchEnd ? lengthDiff : lengthDiff < 0 && pos > matchStart + newLen ? newLen + matchStart - pos : 0;
|
||||
newEnd += move(end);
|
||||
newStart += move(start);
|
||||
newValue += i ? value.slice(matches[i - 1][1], matchStart) + str : str;
|
||||
}
|
||||
insertText(editor, newValue, matches[0][0], matches[l - 1][1], newStart, newEnd);
|
||||
},
|
||||
destroy() {
|
||||
removeHandler();
|
||||
removeSelection();
|
||||
container.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
//#endregion
|
||||
export { createReplaceAPI as t };
|
||||
|
||||
//# sourceMappingURL=replace-BGouct-V.js.map
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { r as createTemplate } from "./core-C0HOUBwg.js";
|
||||
import { p as regexEscape, t as addOverlay, x as updateNode } from "./utils-Cmdnutew.js";
|
||||
//#region src/extensions/search/search.ts
|
||||
var searchTemplate = /* @__PURE__ */ createTemplate("<div style=\"color:#0000;contain:strict;padding:0 var(--_pse) 0 var(--padding-left)\" aria-hidden=true> ");
|
||||
var matchTemplate = /* @__PURE__ */ createTemplate("<span> ");
|
||||
var testBoundary = (str, position, pattern = /[_\p{N}\p{L}]{2}/u) => {
|
||||
if (!position) return false;
|
||||
return pattern.test(str.slice(position - (str.codePointAt(position - 2) > 65535 ? 2 : 1), position + (str.codePointAt(position) > 65535 ? 2 : 1)));
|
||||
};
|
||||
/** Function adding search functionality to an editor. */
|
||||
var createSearchAPI = (editor) => {
|
||||
const container = searchTemplate();
|
||||
const nodes = [container.firstChild];
|
||||
const matchPositions = [];
|
||||
const stopSearch = () => {
|
||||
if (matchPositions[0]) {
|
||||
matchPositions.length = 0;
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
let regex;
|
||||
let lastNode = 0;
|
||||
return {
|
||||
search(str, caseSensitive, wholeWord, useRegExp, selection, filter, pattern) {
|
||||
if (!str) return stopSearch();
|
||||
if (!useRegExp) str = regexEscape(str);
|
||||
const value = editor.value;
|
||||
const searchStr = selection ? value.slice(...selection) : value;
|
||||
const offset = selection ? selection[0] : 0;
|
||||
let match;
|
||||
let l;
|
||||
let index;
|
||||
let i = 0;
|
||||
try {
|
||||
regex = RegExp(str, caseSensitive ? "gum" : "gumi");
|
||||
while (match = regex.exec(searchStr)) {
|
||||
l = match[0].length;
|
||||
index = match.index + offset;
|
||||
if (!l) regex.lastIndex += value.codePointAt(index) > 65535 ? 2 : 1;
|
||||
if (wholeWord && (testBoundary(value, index, pattern) || testBoundary(value, index + l, pattern))) continue;
|
||||
if (!filter || filter(index, index + l)) matchPositions[i++] = [index, index + l];
|
||||
}
|
||||
} catch (e) {
|
||||
stopSearch();
|
||||
return e.message;
|
||||
}
|
||||
if (i) {
|
||||
matchPositions.length = i;
|
||||
l = Math.min(i * 2, 2e4);
|
||||
for (i = nodes.length; i <= l;) {
|
||||
nodes[i++] = matchTemplate();
|
||||
nodes[i++] = new Text();
|
||||
}
|
||||
for (i = l; i < lastNode;) nodes[++i].remove();
|
||||
if (lastNode < l) container.append(...nodes.slice(lastNode + 1, l + 1));
|
||||
let prevEnd = 0;
|
||||
for (i = 0; i < l;) {
|
||||
const [start, end] = matchPositions[i / 2];
|
||||
updateNode(nodes[i++], value.slice(prevEnd, start));
|
||||
updateNode(nodes[i++].firstChild, value.slice(start, prevEnd = end));
|
||||
}
|
||||
updateNode(nodes[l], value.slice(prevEnd));
|
||||
if (!container.parentNode) addOverlay(editor, container);
|
||||
lastNode = l;
|
||||
} else stopSearch();
|
||||
},
|
||||
container,
|
||||
get regex() {
|
||||
return regex;
|
||||
},
|
||||
matches: matchPositions,
|
||||
stopSearch
|
||||
};
|
||||
};
|
||||
//#endregion
|
||||
export { matchTemplate as n, searchTemplate as r, createSearchAPI as t };
|
||||
|
||||
//# sourceMappingURL=search-CvZenEEf.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
.prism-search{display:grid;border:1px solid var(--pce-widget-border);grid:auto / 1em auto;gap:.3em;padding:.3em;border-radius:.3em;margin:-99in 0 0;line-height:1.5;color:var(--pce-widget-color);background:var(--pce-widget-bg);position:sticky;top:.5em;right:.5em;left:.5em;z-index:4;pointer-events:auto}.prism-search-container *{box-sizing:border-box}.prism-search button{all:unset;cursor:pointer}.pce-readonly .prism-search{grid:auto / auto}.prism-search>div>div{display:flex}.pce-input input{padding:0 0 0 .3em;width:calc(var(--search-width, 20em) - 10ch - 4px);font:inherit;border:none;background:#0000;color:var(--pce-widget-color);outline:0}.pce-input ::placeholder{color:inherit;opacity:.6}.pce-match-count{margin:1px auto 1px 0}.pce-match-count,.pce-input{font-family:Arial,Helvetica,sans-serif}.pce-options{color:var(--pce-widget-color-options);font-size:80%;gap:.25em}.pce-options button{padding:0 .2em;border-radius:.2em;width:1.2em;text-align:center}.pce-options button span{pointer-events:none}.prism-search button:focus-visible{outline:1px solid var(--pce-widget-focus);z-index:1}.pce-input,.pce-options button{box-shadow:0 0 0 1px var(--_border, var(--pce-widget-border));margin:1px}.pce-input{display:flex;position:relative;background:var(--pce-widget-bg-input);border-radius:.15em;height:1.5em}.pce-input:focus-within{--_border: var(--pce-widget-focus)}.pce-find button{display:grid;align-items:center;padding:0 .4em}.pce-replace button{width:7.3ch;text-align:center}.pce-replace :last-child{width:2.7ch}.pce-input button{box-shadow:-1px 0 0 0 var(--pce-widget-border);margin-left:1px}.pce-prev:before{transform:rotate(-90deg)}.pce-next:before,.pce-expand:before{transform:rotate(90deg)}@media(hover:hover){.prism-search button:hover{background:var(--pce-widget-bg-hover)}}.pce-options button[aria-pressed=true]{background:var(--pce-widget-bg-active);color:var(--pce-widget-color-active);--_border: var(--pce-widget-focus)}.pce-input>:nth-child(3){border-radius:0 .15em .15em 0}.pce-input.pce-error{--_border: var(--pce-widget-error);--pce-widget-focus: var(--_border)}.search-error{display:none;position:absolute;top:100%;box-shadow:inherit;width:100%;white-space:normal;word-break:break-word;left:0;background:var(--pce-widget-bg-error);padding:.5em;z-index:1}.pce-error:focus-within .search-error{display:block}button.pce-close{display:grid;width:1.3em;height:1.3em;place-items:center;margin-left:auto;border-radius:.3em}.pce-find button:before,.pce-expand:before,.pce-close:before{content:"";background:currentColor;height:1.2em;opacity:.75}.pce-close:before{clip-path:polygon(50% 44.34%,0% 74.34%,9.43% 80%,9.43% 20%,0% 25.66%,50% 55.66%,100% 25.66%,90.57% 20%,50% 44.34%,9.43% 20%,9.43% 80%,50% 55.66%,90.57% 80%,100% 74.34%);width:.72em}.prism-search>div{display:grid;gap:.25em;width:var(--search-width, 20em)}button.pce-expand{display:grid;place-items:center;border-radius:.25em;margin:-.15em}.pce-readonly .pce-expand,[aria-expanded=false]+div .pce-replace,.pce-readonly .pce-replace{display:none}.pce-expand[aria-expanded=true]:before{transform:rotate(180deg)}.pce-find button:before,.pce-expand:before{width:.84em;clip-path:polygon(50% 28.96%,0% 63.96%,10.1% 71.04%,50% 43.11%,89.9% 71.04%,100% 63.96%)}button.pce-regex{display:flex}.pce-regex span{display:flex;align-items:flex-end}.pce-regex span:before{content:"";width:.3em;height:.3em;background:currentColor;margin:0 0 .2em .1em}.pce-regex span:after{content:"*";line-height:0;font-size:140%;margin-bottom:.5em}button.pce-in-selection{display:grid;place-items:center}button.pce-whole{position:relative}.pce-whole:after{content:"";position:absolute;bottom:.31em;right:.27em;left:.27em;height:.25em;clip-path:inset(1px -1px -1px);box-shadow:0 0 0 1px}.pce-in-selection:before{content:"";height:80%;width:90%;background:currentColor;clip-path:polygon(0 16%,80% 16%,80% 24%,0 24%,0 46%,100% 46%,100% 54%,0 54%,0 76%,60% 76%,60% 84%,0 84%)}.pce-matches span{background:var(--pce-search-match)}.pce-matches :empty{padding:0 2px;margin:0 -2px}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { t as createSearchAPI } from "./search-CvZenEEf.js";
|
||||
//#region src/extensions/search/selection.ts
|
||||
/**
|
||||
* Extension that highlights selection matches in an editor.
|
||||
* @param caseSensitive Whether or not matches must have the same case. Defaults to false.
|
||||
* @param minLength Minimum length needed to perform a search. Defaults to 1.
|
||||
* @param maxLength Maximum length at which to perform a search. Defaults to 200.
|
||||
|
||||
* Lower values of `minLength` and higher values of `maxLength` can impact performance.
|
||||
*
|
||||
* The CSS-selector `.selection-matches span` can be used to style the matches.
|
||||
*/
|
||||
var highlightSelectionMatches = (caseSensitive, minLength = 1, maxLength = 200) => {
|
||||
const self = (editor) => {
|
||||
const searchAPI = self.api = createSearchAPI(editor);
|
||||
const container = searchAPI.container;
|
||||
container.style.zIndex = -1;
|
||||
container.className = "selection-matches";
|
||||
editor.on("selectionChange", ([start, end], value) => {
|
||||
value = editor.focused ? value.slice(start, end) : "";
|
||||
start += value.search(/\S/);
|
||||
value = value.trim();
|
||||
let l = value.length;
|
||||
searchAPI.search(minLength > l || l > maxLength ? "" : value, caseSensitive, false, false, void 0, (mStart, mEnd) => mStart > start || mEnd <= start);
|
||||
});
|
||||
};
|
||||
return self;
|
||||
};
|
||||
/**
|
||||
* Extension that highlights all instances of the word the cursor is on if there's no selection.
|
||||
* @param filter Function that can filter away matches based on their position.
|
||||
* The filter can be changed later using the `setFilter` method.
|
||||
* @param includeHyphens A function returning whether or not hyphens should be included in the search.
|
||||
* For languages that don't commonly use hyphens as an operator (such as CSS), it makes sense to
|
||||
* return true. If this parameter is omitted, hyphens are not included.
|
||||
*
|
||||
* The CSS-selector `.word-matches span` can be used to style the matches.
|
||||
*
|
||||
* @example
|
||||
* This filters away all words that start inside a string, comment or keyword or regex token.
|
||||
* Different filter functions should be chosen based on the language.
|
||||
* ```
|
||||
* const selector = ".string, .comment, .keyword, .regex"
|
||||
* const filter = start => !getClosestToken(editor, selector, 0, 0, start)
|
||||
* const includeHyphens = position => getLanguage(editor, position) == "css"
|
||||
*
|
||||
* editor.addExtensions(
|
||||
* highlightCurrentWord(filter, includeHyphens)
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
var highlightCurrentWord = (filter, includeHyphens) => {
|
||||
const self = (editor) => {
|
||||
let noHighlight = false;
|
||||
let searchAPI = self.api = createSearchAPI(editor);
|
||||
let container = searchAPI.container;
|
||||
container.style.zIndex = -1;
|
||||
container.className = "word-matches";
|
||||
editor.on("update", () => noHighlight = true);
|
||||
editor.on("selectionChange", ([start, end], value) => {
|
||||
if (start < end || !editor.focused || noHighlight) searchAPI.search("");
|
||||
else {
|
||||
let group = `[_$\\p{L}\\d${includeHyphens && includeHyphens(start) ? "-" : ""}]`;
|
||||
let before = value.slice(0, start).match(RegExp(group + "*$", "u"));
|
||||
let index = before.index;
|
||||
let word = before[0] + value.slice(start).match(RegExp("^" + group + "*", "u"))[0];
|
||||
searchAPI.search(/^-*(\d|$)/.test(word) || filter && !filter(index, index + word.length) ? "" : word, true, true, false, void 0, filter, RegExp(group + "{2}", "u"));
|
||||
}
|
||||
noHighlight = false;
|
||||
});
|
||||
};
|
||||
self.setFilter = (newFilter) => filter = newFilter;
|
||||
return self;
|
||||
};
|
||||
//#endregion
|
||||
export { highlightSelectionMatches as n, highlightCurrentWord as t };
|
||||
|
||||
//# sourceMappingURL=selection-ClP9zgI_.js.map
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
import { l as preventDefault } from "./core-C0HOUBwg.js";
|
||||
import { h as addTextareaListener, s as getModifierCode, u as isMac } from "./utils-Cmdnutew.js";
|
||||
//#region src/extensions/commands/utils.ts
|
||||
/**
|
||||
* Normalizes modifier keys to a bitmask that's used by {@link getKeysFromEvent}.
|
||||
* All keys are case insensitive.
|
||||
*
|
||||
* Rules:
|
||||
* - `alt` → `1`
|
||||
* - `ctrl` or `control` → `2`
|
||||
* - `meta` or `cmd` → `4`
|
||||
* - `shift` → `8`
|
||||
* - `mod` → `4` on Mac, `2` otherwise
|
||||
*
|
||||
* @param key Key to normalize.
|
||||
* @returns Normalized and lowercase key.
|
||||
* @example
|
||||
* normalizeKey("a") // "0+a"
|
||||
* normalizeKey("+") // "0++"
|
||||
* normalizeKey("ctrl+alt+a") // "3+a"
|
||||
* normalizeKey("10+ALT+a") // "11+a"
|
||||
* normalizeKey("shift+alt+12+a") // "13+a"
|
||||
*/
|
||||
var normalizeKey = (key) => {
|
||||
let code = 0;
|
||||
let parts = key.toLowerCase().split("+");
|
||||
let last = parts.pop() || "+";
|
||||
for (const part of parts) code |= +part || modifierMap[part] || 0;
|
||||
return code + "+" + last;
|
||||
};
|
||||
/**
|
||||
* Gets a `Set` of keys that match a keyboard event.
|
||||
* @param e Keyboard event to get keys for.
|
||||
*/
|
||||
var getKeysFromEvent = (e) => {
|
||||
if (!keyCodeMap[48]) {
|
||||
for (let i = 48; i < 91; i++) keyCodeMap[i] = String.fromCharCode(i > 64 ? i + 32 : i);
|
||||
for (let i = 96; i < 112; i++) keyCodeMap[i] = String.fromCharCode(i < 106 ? i - 48 : i - 64);
|
||||
}
|
||||
const key = e.key || "";
|
||||
const code = getModifierCode(e);
|
||||
const keyBase = keyCodeMap[e.keyCode];
|
||||
const keyShift = keyCodeMapShift[e.keyCode];
|
||||
const hasShift = code > 7;
|
||||
const keyName = (isMac && code == 12 || key == "Dead" || key == "Unidentified") && (hasShift && keyShift || keyBase) || key.toLowerCase();
|
||||
const isNotChar = /\w\w| /.test(keyName.slice(0, 2));
|
||||
const noShift = code & 7;
|
||||
const commands = /* @__PURE__ */ new Set([`${isNotChar || /\p{L}/u.test(keyName) ? code : noShift}+${keyName}`]);
|
||||
if (!isNotChar) {
|
||||
if (noShift && !(isMac && noShift == 1) && keyBase) {
|
||||
commands.add(code + "+" + keyBase);
|
||||
if (hasShift && keyShift) commands.add(noShift + "+" + keyShift);
|
||||
} else if (hasShift) commands.add(code + "+" + keyName);
|
||||
}
|
||||
return commands;
|
||||
};
|
||||
var editorMap = /* @__PURE__ */ new WeakMap();
|
||||
/**
|
||||
* Utility that runs all commands whose key matches the keyboard event until a command
|
||||
* returns a truthy value. In that case `preventDefault` and `stopImmediatePropagation`
|
||||
* are called.
|
||||
*
|
||||
* @param e Keyboard event to run commands for.
|
||||
* @param map Record mapping keys to a list of commands for that key.
|
||||
* @param data Argument to pass to the commands.
|
||||
* @returns `true` if a command returned a truthy value.
|
||||
*
|
||||
* @example
|
||||
* addEventListener("keydown", e => {
|
||||
* const handled = !!runHotkeys(e, map, data)
|
||||
* })
|
||||
*/
|
||||
var runHotkeys = (e, map, data) => {
|
||||
let seen = /* @__PURE__ */ new Set();
|
||||
let firstKey;
|
||||
try {
|
||||
for (const key of getKeysFromEvent(e)) {
|
||||
firstKey ||= key;
|
||||
for (const set of map[key] || []) for (const cmd of set || []) if (!seen.has(cmd)) {
|
||||
if (cmd(data, e, key)) {
|
||||
preventDefault(e);
|
||||
return true;
|
||||
}
|
||||
seen.add(cmd);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
map.any?.forEach((set) => set?.forEach((cmd) => cmd(data, e, firstKey)));
|
||||
}
|
||||
};
|
||||
var getMap = (editor) => {
|
||||
let map = editorMap.get(editor);
|
||||
if (!map) {
|
||||
editorMap.set(editor, map = {});
|
||||
addTextareaListener(editor, "keydown", (e) => {
|
||||
runHotkeys(e, map, editor);
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
/**
|
||||
* Registers a command for the specified key.
|
||||
*
|
||||
* @param editor Editor to add the command to.
|
||||
* @param key Key the command will run for.
|
||||
* @param command Command for the specified key.
|
||||
* @param precedence Positive integer denoting the precedence of the command where `0`
|
||||
* is the highest precedence. Defaults to `2`.
|
||||
* @returns Function to remove the hotkey.
|
||||
*/
|
||||
var addEditorHotkey = (editor, key, command, precedence) => {
|
||||
return addHotkey(getMap(editor), key, command, precedence);
|
||||
};
|
||||
/**
|
||||
* Registers a command for the specified key to the hotkey map.
|
||||
*
|
||||
* @param map Map to add the command to.
|
||||
* @param key Key the command will run for.
|
||||
* @param command Command for the specified key.
|
||||
* @param precedence Positive integer denoting the precedence of the command where `0`
|
||||
* is the highest precedence. Defaults to `2`.
|
||||
* @returns Function to remove the hotkey.
|
||||
*/
|
||||
var addHotkey = (map, key, command, precedence = 2) => {
|
||||
let set = (map[key == "any" ? key : normalizeKey(key)] ||= [])[precedence] ||= /* @__PURE__ */ new Set();
|
||||
set.add(command);
|
||||
return () => {
|
||||
set.delete(command);
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Registers a sequential hotkey to the specifed editor.
|
||||
*
|
||||
* @param editor Editor to add the sequence to.
|
||||
* @param sequence Sequence of keys to press for the command to be executed.
|
||||
* @param command Command to execute when the sequence is pressed.
|
||||
* @returns Function to remove the hotkey.
|
||||
*/
|
||||
var addEditorHotkeySequence = (editor, sequence, command, options) => {
|
||||
return addHotkeySequence(getMap(editor), sequence, command, options);
|
||||
};
|
||||
/**
|
||||
* Registers a sequential hotkey to the specifed hotkey map.
|
||||
*
|
||||
* @param map Map to add the sequence to.
|
||||
* @param sequence Sequence of keys to press for the command to be executed.
|
||||
* @param command Command to execute when the sequence is pressed.
|
||||
* @returns Function to remove the hotkey.
|
||||
*/
|
||||
var addHotkeySequence = (map, sequence, command, options = {}) => {
|
||||
let lastTimestamp;
|
||||
let handledEvent;
|
||||
let current = 0;
|
||||
let last = sequence.length - 1;
|
||||
let removeHandler;
|
||||
if (last < 0) throw Error("Sequence must have a least one key");
|
||||
const { timeout = 2e3, precedence, preventDefault = true } = options;
|
||||
const handleStep = (data, e, key) => {
|
||||
if (current) removeHandler();
|
||||
if (lastTimestamp + timeout < (lastTimestamp = Date.now())) return;
|
||||
handledEvent = e;
|
||||
if (current == last) {
|
||||
current = 0;
|
||||
return command(data, e, key);
|
||||
}
|
||||
if (preventDefault) e.preventDefault();
|
||||
removeHandler = addHotkey(map, sequence[++current], handleStep, 0);
|
||||
};
|
||||
const removeStart = addHotkey(map, sequence[0], (data, e, key) => {
|
||||
if (handledEvent != e) {
|
||||
lastTimestamp = Date.now();
|
||||
current = 0;
|
||||
handleStep(data, e, key);
|
||||
}
|
||||
}, precedence);
|
||||
const removeAny = addHotkey(map, "any", (_, e) => {
|
||||
if (current && handledEvent != e && !isModifierKey(e)) {
|
||||
current = 0;
|
||||
removeHandler();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
removeStart();
|
||||
removeAny();
|
||||
removeHandler?.();
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Returns whether the key pressed is Alt, Shift, Control, or Meta.
|
||||
* @param e Keyboard event to check.
|
||||
*/
|
||||
var isModifierKey = (e) => {
|
||||
return modifierCodes.has(e.keyCode);
|
||||
};
|
||||
var modifierCodes = /* @__PURE__ */ new Set([
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
91,
|
||||
92,
|
||||
224,
|
||||
225
|
||||
]);
|
||||
var mod = isMac ? 4 : 2;
|
||||
var keyCodeMap = {
|
||||
32: " ",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
};
|
||||
var keyCodeMapShift = {
|
||||
48: ")",
|
||||
49: "!",
|
||||
50: "@",
|
||||
51: "#",
|
||||
52: "$",
|
||||
53: "%",
|
||||
54: "^",
|
||||
55: "&",
|
||||
56: "*",
|
||||
57: "(",
|
||||
59: ":",
|
||||
61: "+",
|
||||
173: "_",
|
||||
186: ":",
|
||||
187: "+",
|
||||
188: "<",
|
||||
189: "_",
|
||||
190: ">",
|
||||
191: "?",
|
||||
192: "~",
|
||||
219: "{",
|
||||
220: "|",
|
||||
221: "}",
|
||||
222: "\""
|
||||
};
|
||||
var modifierMap = {
|
||||
alt: 1,
|
||||
control: 2,
|
||||
ctrl: 2,
|
||||
meta: 4,
|
||||
cmd: 4,
|
||||
shift: 8,
|
||||
mod: isMac ? 4 : 2
|
||||
};
|
||||
//#endregion
|
||||
export { getKeysFromEvent as a, normalizeKey as c, addHotkeySequence as i, runHotkeys as l, addEditorHotkeySequence as n, isModifierKey as o, addHotkey as r, mod as s, addEditorHotkey as t };
|
||||
|
||||
//# sourceMappingURL=utils-ChVWFtO9.js.map
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import { n as escapeHtml } from "./core-DEy9UQvI.js";
|
||||
import { c as numLines, i as doc, t as addListener } from "./core-C0HOUBwg.js";
|
||||
//#region src/utils/local.ts
|
||||
var scrollToEl = (editor, el, paddingTop = 0) => {
|
||||
const style = editor.container.style;
|
||||
style.setProperty("--_sp", "var(--pce-scroll-padding, 2ch)");
|
||||
style.scrollPaddingBlock = `calc(var(--_sp) + ${paddingTop}px) calc(var(--_sp) + ${isChrome && !el.offsetWidth ? el.offsetHeight : 0}px)`;
|
||||
el.scrollIntoView({ block: "nearest" });
|
||||
style.scrollPaddingBlock = "";
|
||||
style.removeProperty("--_sp");
|
||||
};
|
||||
var getLineStart = (text, position) => position ? text.lastIndexOf("\n", position - 1) + 1 : 0;
|
||||
var getLineEnd = (text, position) => (position = text.indexOf("\n", position)) + 1 ? position : text.length;
|
||||
var addListener2 = (element, type, listener, options) => {
|
||||
addListener(element, type, listener, options);
|
||||
return () => element.removeEventListener(type, listener, options);
|
||||
};
|
||||
var addTextareaListener = (editor, type, listener, options) => addListener2(editor.textarea, type, listener, options);
|
||||
var getStyleValue = (el, prop) => parseFloat(getComputedStyle(el)[prop]);
|
||||
var getPosition = (editor, el) => {
|
||||
const rect1 = el.getBoundingClientRect();
|
||||
const rect2 = editor.lines[0].getBoundingClientRect();
|
||||
return {
|
||||
top: rect1.y - rect2.y,
|
||||
bottom: rect2.bottom - rect1.bottom,
|
||||
left: rect1.x - rect2.x,
|
||||
right: rect2.right - rect1.right,
|
||||
height: rect1.height
|
||||
};
|
||||
};
|
||||
var updateNode = (node, text) => {
|
||||
if (node.data != text) node.data = text;
|
||||
};
|
||||
var voidlessLangs = new Set("xml,rss,atom,jsx,tsx,xquery,xeora,xeoracube,actionscript".split(","));
|
||||
var voidTags = /^(?:area|base|w?br|col|embed|hr|img|input|link|meta|source|track)$/;
|
||||
//#endregion
|
||||
//#region src/utils/index.ts
|
||||
/**
|
||||
* If {@link insertText} has been called, this variable stores the editor's selection
|
||||
* before the call. This variable is set to 0 right before {@link insertText} returns.
|
||||
* This can therefore be used inside a `beforeinput` handler to determine if
|
||||
* {@link insertText} fired the event, and if true, get the selection before the call.
|
||||
*
|
||||
* Intended for internal use only.
|
||||
*/
|
||||
var prevSelection;
|
||||
/**
|
||||
* Escapes all regex syntax characters with a backslash and returns the escaped string.
|
||||
*
|
||||
* The returned string is not safe inside a character class.
|
||||
*/
|
||||
var regexEscape = (str) => str.replace(/[$+?|.^*()[\]{}\\]/g, "\\$&");
|
||||
/** Returns the string between the position and the previous \n. */
|
||||
var getLineBefore = (text, position) => text.slice(getLineStart(text, position), position);
|
||||
/**
|
||||
* Gets all lines that are at least partially between `start` and `end`.
|
||||
* @param text Text to search in.
|
||||
* @param start Start of the selection.
|
||||
* @param end End of the selection. Defaults to `start`.
|
||||
* @returns A tuple containing an array of lines, the starting position of the first line,
|
||||
* and the ending position of the last line.
|
||||
*/
|
||||
var getLines = (text, start, end = start) => [
|
||||
text.slice(start = getLineStart(text, start), end = getLineEnd(text, end)).split("\n"),
|
||||
start,
|
||||
end
|
||||
];
|
||||
/**
|
||||
* Searches a full line for a token that matches a selector and contains `position`
|
||||
* within the specified margins. Tokens are searched in reverse document order which means
|
||||
* children are searched before their parents.
|
||||
* @param editor Editor you want to search in.
|
||||
* @param selector CSS selector for the tokens you want to search for.
|
||||
* @param marginLeft How far to the left of the token the position can be. Defaults to 0.
|
||||
* @param marginRight How far to the right of the token the position can be. Defaults to `marginLeft`.
|
||||
* @param position Position to search in. Defaults to `selectionStart`.
|
||||
* @returns A span element if one's found or undefined if not.
|
||||
* @example
|
||||
* This will return a string token if the cursor
|
||||
* is at least 1 character inside a string token
|
||||
* ```javascript
|
||||
* getClosestToken(editor, '.string', -1)
|
||||
* ```
|
||||
*/
|
||||
var getClosestToken = (editor, selector, marginLeft = 0, marginRight = marginLeft, position = editor.getSelection()[0]) => {
|
||||
const value = editor.value;
|
||||
const line = editor.lines[numLines(value, 0, position)];
|
||||
const walker = doc.createTreeWalker(line, 5);
|
||||
let node = walker.lastChild();
|
||||
let offset = getLineEnd(value, position) + 1 - position - node.length;
|
||||
while (-offset <= marginRight && (node = walker.previousNode())) {
|
||||
if (node.lastChild) continue;
|
||||
offset -= node.length || 0;
|
||||
if (offset <= marginLeft) {
|
||||
for (; node != line; node = node.parentNode) if (node.matches?.(selector)) return node;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Gets the current language at a position.
|
||||
* Useful if you want to run different logic based on the language.
|
||||
* @param editor Editor to search in.
|
||||
* @param position Position to search in. Defaults to `selectionStart`.
|
||||
*/
|
||||
var getLanguage = (editor, position) => getClosestToken(editor, "[class*=language-]", 0, 0, position)?.className.match(/language-(\S*)/)[1] || editor.options.language;
|
||||
/**
|
||||
* Inserts text into the editor (unless it's read-only) while keeping undo/redo history.
|
||||
* Focuses the `textarea` if it isn't already.
|
||||
* @param editor Target editor.
|
||||
* @param text Text to insert.
|
||||
* @param start Position to start the insertion. Defaults to `selectionStart`.
|
||||
* @param end Position to end the insertion. Defaults to `start` if specified, else `selectionEnd`.
|
||||
* @param newCursorStart New starting position for the cursor. Defaults to the end of the inserted text.
|
||||
* @param newCursorEnd New ending position for the cursor. Defaults to `newCursorStart`.
|
||||
*/
|
||||
var insertText = (editor, text, start, end, newCursorStart, newCursorEnd) => {
|
||||
if (editor.options.readOnly) return;
|
||||
prevSelection = editor.getSelection();
|
||||
end ??= start;
|
||||
let textarea = editor.textarea;
|
||||
let value = editor.value;
|
||||
let avoidBug = isChrome && !value[end ?? prevSelection[1]] && /\n$/.test(text) && /^$|\n$/.test(value);
|
||||
let removeListener;
|
||||
editor.focused || textarea.focus();
|
||||
if (start != null) textarea.setSelectionRange(start, end);
|
||||
if (newCursorStart != null) removeListener = editor.on("update", () => {
|
||||
textarea.setSelectionRange(newCursorStart, newCursorEnd ?? newCursorStart, prevSelection[2]);
|
||||
removeListener();
|
||||
});
|
||||
isWebKit || textarea.dispatchEvent(new InputEvent("beforeinput", { data: text }));
|
||||
if (isChrome || isWebKit) {
|
||||
if (avoidBug) {
|
||||
textarea.selectionEnd--;
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
if (isWebKit) text += "\n";
|
||||
doc.execCommand(text ? "insertHTML" : "delete", false, escapeHtml(text, /</g, "<"));
|
||||
if (avoidBug) textarea.selectionStart++;
|
||||
} else doc.execCommand(text ? "insertText" : "delete", false, text);
|
||||
prevSelection = 0;
|
||||
};
|
||||
/**
|
||||
* Sets the selection for the `textarea` and synchronously runs the selectionChange listeners.
|
||||
* If you don't want to synchronously run the listeners, use `textarea.setSelectionRange` instead.
|
||||
* @param editor Editor you want to change the selection of.
|
||||
* @param start New selectionStart.
|
||||
* @param end New selectionEnd. Defaults to `start`.
|
||||
* @param direction New direction.
|
||||
*/
|
||||
var setSelection = (editor, start, end = start, direction) => {
|
||||
let textarea = editor.textarea;
|
||||
let removeHandler = addListener2(textarea, "focus", (e) => {
|
||||
let target = e.relatedTarget;
|
||||
target ? target.focus() : textarea.blur();
|
||||
});
|
||||
textarea.setSelectionRange(start, end, direction);
|
||||
removeHandler();
|
||||
textarea.dispatchEvent(new Event("selectionchange"));
|
||||
};
|
||||
var userAgent = doc ? navigator.userAgent : "";
|
||||
/** Whether the user is on an Apple-based system. */
|
||||
var isMac = doc ? /Mac|iPhone|iP[ao]d/.test(navigator.platform) : false;
|
||||
/** Whether the browser is Chromium based. */
|
||||
var isChrome = /Chrome\//.test(userAgent);
|
||||
/** Whether the browser uses the WebKit browser engine. */
|
||||
var isWebKit = !isChrome && /AppleWebKit\//.test(userAgent);
|
||||
/**
|
||||
* Returns a 4 bit integer where each bit represents whether
|
||||
* each modifier is pressed in the order Shift, Meta, Ctrl, Alt.
|
||||
*
|
||||
* ```javascript
|
||||
* e.shiftKey && !e.metaKey && e.ctrlKey && e.altKey
|
||||
* // is equivalent to
|
||||
* getModifierCode(e) == 0b1011
|
||||
* ```
|
||||
*/
|
||||
var getModifierCode = (e) => e.altKey + e.ctrlKey * 2 + e.metaKey * 4 + e.shiftKey * 8;
|
||||
/**
|
||||
* Adds an overlay by appending the element to the editor's overlays.
|
||||
* Equivalent to calling `editor.lines[0].append(overlay)`.
|
||||
* @param editor Editor or code block you want to add an overlay to.
|
||||
* @param overlay The overlay you want to add.
|
||||
*/
|
||||
var addOverlay = (editor, overlay) => editor.lines[0].append(overlay);
|
||||
/**
|
||||
* Gets the position of the cursor in the document. It returns a tuple with three numbers:
|
||||
*
|
||||
* 1. The line number of the cursor
|
||||
* 2. The 1-based column of the cursor
|
||||
* 3. Number of characters selected
|
||||
*
|
||||
* @example
|
||||
* const [line, col, selected] = getDocumentPosition(editor)
|
||||
*/
|
||||
var getDocumentPosition = (editor) => {
|
||||
let [start, end, dir] = editor.getSelection();
|
||||
let pos = dir < "f" ? start : end;
|
||||
let value = editor.value;
|
||||
let col = 0;
|
||||
let chars = 0;
|
||||
let tabSize = editor.options.tabSize || 2;
|
||||
for (const char of getLineBefore(value, pos)) col += char == " " ? tabSize - col % tabSize : 1;
|
||||
for (const _ of value.slice(start, end)) chars++;
|
||||
return [
|
||||
editor.activeLine,
|
||||
col + 1,
|
||||
chars
|
||||
];
|
||||
};
|
||||
//#endregion
|
||||
export { voidlessLangs as C, voidTags as S, getLineStart as _, getLineBefore as a, scrollToEl as b, insertText as c, isWebKit as d, prevSelection as f, getLineEnd as g, addTextareaListener as h, getLanguage as i, isChrome as l, setSelection as m, getClosestToken as n, getLines as o, regexEscape as p, getDocumentPosition as r, getModifierCode as s, addOverlay as t, isMac as u, getPosition as v, updateNode as x, getStyleValue as y };
|
||||
|
||||
//# sourceMappingURL=utils-Cmdnutew.js.map
|
||||
@@ -0,0 +1,21 @@
|
||||
# THE SINGLE SOURCE of the openwrt/luci commit this package borrows two BUILD TOOLS from, and of
|
||||
# their checksums. Sourced as shell (`. luci-upstream.pin`) by update-po.sh and by tools/t0.sh.
|
||||
#
|
||||
# Both are downloaded over the network and then EXECUTED as gates, so a moving branch would make
|
||||
# the gate whatever upstream pushed last:
|
||||
#
|
||||
# jsmin.c compiled, then run over every module and the OUTPUT re-parsed. jsmin's
|
||||
# regex-vs-division lookback can eat the rest of a file and still EXIT 0, which
|
||||
# is why the gate is a round-trip and not a grep for the pattern.
|
||||
# i18n-scan.pl run by perl; decides whether the translation catalogue is complete.
|
||||
#
|
||||
# The pin matches luci-theme-footstrap's. That is deliberate rather than incidental: this package
|
||||
# is checked with the same two linters as the theme it plugs into, and two companion projects
|
||||
# disagreeing about which jsmin decides their JS is safe would be a difference nobody would look
|
||||
# for. Bump them together.
|
||||
#
|
||||
# To bump: change LUCI_PIN, re-fetch both files at the new commit, update their sha256s. The
|
||||
# sha256 is what actually holds the content — the commit only says which file to fetch.
|
||||
LUCI_PIN=5cf20c5741dc34eea0faaf7324ef3751b05fa1f3
|
||||
JSMIN_SHA256=0568b14873f27f339cf66a9c43603d0bc8ca501ff62522ea924c6e8daf1be215
|
||||
I18N_SCAN_SHA256=a1823d1fdd942f31a105e1e53b401e146bc10527f231e016930b1a5123ef21d3
|
||||
@@ -0,0 +1,411 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.9\n"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to copy"
|
||||
msgstr "%d elemento(s) para copiar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to move"
|
||||
msgstr "%d elemento(s) para mover"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1279
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1706
|
||||
msgid "%d selected"
|
||||
msgstr "%d seleccionado(s)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:987
|
||||
msgid "%s could not be removed (%s). Delete it and everything inside?"
|
||||
msgstr "No se pudo eliminar %s (%s). ¿Eliminarlo junto con todo lo que contiene?"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:978
|
||||
msgid "%s is a directory. Delete it and everything inside?"
|
||||
msgstr "%s es un directorio. ¿Eliminarlo junto con todo su contenido?"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:608
|
||||
msgid "%s is not text; opening it as hex."
|
||||
msgstr "%s no es texto; abriéndolo como hexadecimal."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1168
|
||||
msgid "%s was not moved"
|
||||
msgstr "%s no se movió"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:931
|
||||
msgid "%s was not pasted"
|
||||
msgstr "%s no se pegó"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
msgid "A name may not contain a slash."
|
||||
msgstr "Un nombre no puede contener una barra inclinada."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:449
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:508
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:559
|
||||
msgid "Actions"
|
||||
msgstr "Acciones"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:864
|
||||
msgid "Apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:852
|
||||
msgid "Apply to everything inside"
|
||||
msgstr "Aplicar a todo lo que hay dentro"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:856
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1696
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:883
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:884
|
||||
msgid "Cannot change %s"
|
||||
msgstr "No se puede cambiar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Cannot change permissions"
|
||||
msgstr "No se pueden cambiar los permisos"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1002
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1003
|
||||
msgid "Cannot create directory"
|
||||
msgstr "No se puede crear el directorio"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1011
|
||||
msgid "Cannot create file"
|
||||
msgstr "No se puede crear el archivo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:991
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:992
|
||||
msgid "Cannot delete %s"
|
||||
msgstr "No se puede eliminar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:574
|
||||
msgid "Cannot download %s"
|
||||
msgstr "No se puede descargar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:589
|
||||
msgid "Cannot edit %s"
|
||||
msgstr "No se puede editar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1171
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1172
|
||||
msgid "Cannot move %s"
|
||||
msgstr "No se puede mover %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:747
|
||||
msgid "Cannot open %s"
|
||||
msgstr "No se puede abrir %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:695
|
||||
msgid "Cannot open the editor"
|
||||
msgstr "No se puede abrir el editor"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Cannot paste"
|
||||
msgstr "No se puede pegar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:934
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:935
|
||||
msgid "Cannot paste %s"
|
||||
msgstr "No se puede pegar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:349
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:368
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:696
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:777
|
||||
msgid "Cannot read %s"
|
||||
msgstr "No se puede leer %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:951
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:952
|
||||
msgid "Cannot rename %s"
|
||||
msgstr "No se puede renombrar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:807
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:826
|
||||
msgid "Cannot save %s"
|
||||
msgstr "No se puede guardar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1035
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1038
|
||||
msgid "Cannot upload %s"
|
||||
msgstr "No se puede cargar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:661
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:769
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1280
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1301
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1708
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1722
|
||||
msgid "Copy"
|
||||
msgstr "Copiar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:517
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1283
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1304
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1710
|
||||
msgid "Delete"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:976
|
||||
msgid "Delete %d item(s)? This cannot be undone."
|
||||
msgstr "¿Eliminar %d elemento(s)? Esta acción no se puede deshacer."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1724
|
||||
msgid "Delete selected"
|
||||
msgstr "Eliminar seleccionado"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "Directory"
|
||||
msgstr "Directorio"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1711
|
||||
msgid "Done"
|
||||
msgstr "Listo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:511
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1292
|
||||
msgid "Download"
|
||||
msgstr "Descargar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:509
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "File"
|
||||
msgstr "Archivo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1783
|
||||
msgid "Files"
|
||||
msgstr "Archivos"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:649
|
||||
msgid "Find and replace"
|
||||
msgstr "Buscar y reemplazar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1686
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1717
|
||||
msgid "Go"
|
||||
msgstr "Ir"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "List"
|
||||
msgstr "Lista"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1740
|
||||
msgid "Long press a file for actions"
|
||||
msgstr "Mantenga pulsado un archivo para realizar acciones"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:448
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:504
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:558
|
||||
msgid "Modified"
|
||||
msgstr "Modificado"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1281
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1302
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1709
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1723
|
||||
msgid "Move"
|
||||
msgstr "Mover"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1162
|
||||
msgid "Move %d item(s) to %s?"
|
||||
msgstr "¿Mover %d elemento(s) a %s?"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1163
|
||||
msgid "Moving %d item(s)…"
|
||||
msgstr "Moviendo %d elemento(s)…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:440
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:481
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:554
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:999
|
||||
msgid "New directory name:"
|
||||
msgstr "Nuevo nombre de directorio:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1317
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1721
|
||||
msgid "New file"
|
||||
msgstr "Nuevo archivo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1007
|
||||
msgid "New file name:"
|
||||
msgstr "Nuevo nombre de archivo:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1316
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1720
|
||||
msgid "New folder"
|
||||
msgstr "Nueva carpeta"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Nothing selected"
|
||||
msgstr "Nada seleccionado"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1291
|
||||
msgid "Open as hex"
|
||||
msgstr "Abrir como hexadecimal"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:848
|
||||
msgid "Owner (user:group)"
|
||||
msgstr "Propietario (usuario:grupo)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1318
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1695
|
||||
msgid "Paste here"
|
||||
msgstr "Pegar aquí"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1748
|
||||
msgid "Path"
|
||||
msgstr "Ruta"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:447
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:500
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:557
|
||||
msgid "Permissions"
|
||||
msgstr "Permisos"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:844
|
||||
msgid "Permissions (octal)"
|
||||
msgstr "Permisos (octal)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Permissions must be three or four octal digits, e.g. 644."
|
||||
msgstr "Los permisos deben constar de tres o cuatro dígitos octales, por ejemplo, 644."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:513
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1294
|
||||
msgid "Properties"
|
||||
msgstr "Propiedades"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:842
|
||||
msgid "Properties of %s"
|
||||
msgstr "Propiedades de %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1321
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1719
|
||||
msgid "Refresh"
|
||||
msgstr "Refrescar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:515
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1293
|
||||
msgid "Rename"
|
||||
msgstr "Renombrar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:945
|
||||
msgid "Rename \"%s\" to:"
|
||||
msgstr "Renombrar «%s» a:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:656
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:764
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:805
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:823
|
||||
msgid "Saved."
|
||||
msgstr "Guardado."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:788
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:817
|
||||
msgid "Saving…"
|
||||
msgstr "Guardando…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:797
|
||||
msgid "Saving… %d%%"
|
||||
msgstr "Guardando... %d%%"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1298
|
||||
msgid "Select"
|
||||
msgstr "Seleccionar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:487
|
||||
msgid "Select %s"
|
||||
msgstr "Seleccionar %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1299
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1320
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1707
|
||||
msgid "Select all"
|
||||
msgstr "Seleccionar todo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:446
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:499
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:556
|
||||
msgid "Size"
|
||||
msgstr "Tamaño"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:932
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1169
|
||||
msgid "Something with that name is already here, and nothing is overwritten."
|
||||
msgstr "Ya existe algo con ese nombre, y nada se sobrescribirá."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Source and destination are the same directory."
|
||||
msgstr "El origen y el destino son el mismo directorio."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:590
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:748
|
||||
msgid "The file is %.1f MB; this editor opens files up to 1 MB."
|
||||
msgstr "El archivo es %.1f MB; este editor abre archivos de hasta 1 MB."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:532
|
||||
msgid "This directory is empty."
|
||||
msgstr "Este directorio está vacío."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Tick the files you want first."
|
||||
msgstr "Seleccione primero los archivos que desee."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "Tiles"
|
||||
msgstr "Íconos"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:555
|
||||
msgid "Type"
|
||||
msgstr "Tipo"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1687
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1718
|
||||
msgid "Up"
|
||||
msgstr "Arriba"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1728
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1731
|
||||
msgid "Upload"
|
||||
msgstr "Cargar"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1037
|
||||
msgid "Uploaded %d of %d…"
|
||||
msgstr "Cargado %d de %d…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1026
|
||||
msgid "Uploading %d file(s)…"
|
||||
msgstr "Cargando %d archivo(s)…"
|
||||
@@ -0,0 +1,413 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to copy"
|
||||
msgstr "Скопировать: %d шт."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to move"
|
||||
msgstr "Переместить: %d шт."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1279
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1706
|
||||
msgid "%d selected"
|
||||
msgstr "Выбрано: %d"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:987
|
||||
msgid "%s could not be removed (%s). Delete it and everything inside?"
|
||||
msgstr "%s удалить не удалось (%s). Удалить каталог вместе со всем содержимым?"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:978
|
||||
msgid "%s is a directory. Delete it and everything inside?"
|
||||
msgstr "%s — каталог. Удалить его вместе со всем содержимым?"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:608
|
||||
msgid "%s is not text; opening it as hex."
|
||||
msgstr "%s — не текст; открываю как hex."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1168
|
||||
msgid "%s was not moved"
|
||||
msgstr "%s не перенесён"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:931
|
||||
msgid "%s was not pasted"
|
||||
msgstr "%s не вставлен"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
msgid "A name may not contain a slash."
|
||||
msgstr "Имя не может содержать косую черту."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:449
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:508
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:559
|
||||
msgid "Actions"
|
||||
msgstr "Действия"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:864
|
||||
msgid "Apply"
|
||||
msgstr "Применить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:852
|
||||
msgid "Apply to everything inside"
|
||||
msgstr "Применить ко всему содержимому"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:856
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1696
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:883
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:884
|
||||
msgid "Cannot change %s"
|
||||
msgstr "Не удалось изменить %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Cannot change permissions"
|
||||
msgstr "Не удалось изменить права"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1002
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1003
|
||||
msgid "Cannot create directory"
|
||||
msgstr "Не удалось создать каталог"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1011
|
||||
msgid "Cannot create file"
|
||||
msgstr "Не удалось создать файл"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:991
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:992
|
||||
msgid "Cannot delete %s"
|
||||
msgstr "Не удалось удалить %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:574
|
||||
msgid "Cannot download %s"
|
||||
msgstr "Не удалось скачать %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:589
|
||||
msgid "Cannot edit %s"
|
||||
msgstr "Не удалось открыть %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1171
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1172
|
||||
msgid "Cannot move %s"
|
||||
msgstr "Не удалось перенести %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:747
|
||||
msgid "Cannot open %s"
|
||||
msgstr "Не удалось открыть %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:695
|
||||
msgid "Cannot open the editor"
|
||||
msgstr "Не удалось открыть редактор"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Cannot paste"
|
||||
msgstr "Не удалось вставить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:934
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:935
|
||||
msgid "Cannot paste %s"
|
||||
msgstr "Не удалось вставить %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:349
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:368
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:696
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:777
|
||||
msgid "Cannot read %s"
|
||||
msgstr "Не удалось прочитать %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:951
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:952
|
||||
msgid "Cannot rename %s"
|
||||
msgstr "Не удалось переименовать %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:807
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:826
|
||||
msgid "Cannot save %s"
|
||||
msgstr "Не удалось сохранить %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1035
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1038
|
||||
msgid "Cannot upload %s"
|
||||
msgstr "Не удалось загрузить %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:661
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:769
|
||||
msgid "Close"
|
||||
msgstr "Закрыть"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1280
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1301
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1708
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1722
|
||||
msgid "Copy"
|
||||
msgstr "Копировать"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:517
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1283
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1304
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1710
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:976
|
||||
msgid "Delete %d item(s)? This cannot be undone."
|
||||
msgstr "Удалить объектов: %d? Это необратимо."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1724
|
||||
msgid "Delete selected"
|
||||
msgstr "Удалить выбранное"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "Directory"
|
||||
msgstr "Каталог"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1711
|
||||
msgid "Done"
|
||||
msgstr "Готово"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:511
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1292
|
||||
msgid "Download"
|
||||
msgstr "Скачать"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:509
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Edit"
|
||||
msgstr "Изменить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1783
|
||||
msgid "Files"
|
||||
msgstr "Файлы"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:649
|
||||
msgid "Find and replace"
|
||||
msgstr "Поиск и замена"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1686
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1717
|
||||
msgid "Go"
|
||||
msgstr "Перейти"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "List"
|
||||
msgstr "Список"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1740
|
||||
msgid "Long press a file for actions"
|
||||
msgstr "Долгое нажатие по файлу — действия"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:448
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:504
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:558
|
||||
msgid "Modified"
|
||||
msgstr "Изменён"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1281
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1302
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1709
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1723
|
||||
msgid "Move"
|
||||
msgstr "Переместить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1162
|
||||
msgid "Move %d item(s) to %s?"
|
||||
msgstr "Скопировать: %d шт."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1163
|
||||
msgid "Moving %d item(s)…"
|
||||
msgstr "Перенос: %d шт…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:440
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:481
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:554
|
||||
msgid "Name"
|
||||
msgstr "Имя"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:999
|
||||
msgid "New directory name:"
|
||||
msgstr "Имя нового каталога:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1317
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1721
|
||||
msgid "New file"
|
||||
msgstr "Новый файл"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1007
|
||||
msgid "New file name:"
|
||||
msgstr "Имя нового файла:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1316
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1720
|
||||
msgid "New folder"
|
||||
msgstr "Новый каталог"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Nothing selected"
|
||||
msgstr "Ничего не выбрано"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1291
|
||||
msgid "Open as hex"
|
||||
msgstr "Открыть как hex"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:848
|
||||
msgid "Owner (user:group)"
|
||||
msgstr "Владелец (пользователь:группа)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1318
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1695
|
||||
msgid "Paste here"
|
||||
msgstr "Вставить сюда"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1748
|
||||
msgid "Path"
|
||||
msgstr "Путь"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:447
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:500
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:557
|
||||
msgid "Permissions"
|
||||
msgstr "Права"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:844
|
||||
msgid "Permissions (octal)"
|
||||
msgstr "Права (восьмеричные)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Permissions must be three or four octal digits, e.g. 644."
|
||||
msgstr "Права задаются тремя или четырьмя восьмеричными цифрами, например 644."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:513
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1294
|
||||
msgid "Properties"
|
||||
msgstr "Свойства"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:842
|
||||
msgid "Properties of %s"
|
||||
msgstr "Свойства %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1321
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1719
|
||||
msgid "Refresh"
|
||||
msgstr "Обновить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:515
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1293
|
||||
msgid "Rename"
|
||||
msgstr "Переименовать"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:945
|
||||
msgid "Rename \"%s\" to:"
|
||||
msgstr "Переименовать «%s» в:"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:656
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:764
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:805
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:823
|
||||
msgid "Saved."
|
||||
msgstr "Сохранено."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:788
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:817
|
||||
msgid "Saving…"
|
||||
msgstr "Сохранение…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:797
|
||||
msgid "Saving… %d%%"
|
||||
msgstr "Сохранение… %d%%"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1298
|
||||
msgid "Select"
|
||||
msgstr "Выбрать"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:487
|
||||
msgid "Select %s"
|
||||
msgstr "Выбрать %s"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1299
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1320
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1707
|
||||
msgid "Select all"
|
||||
msgstr "Выбрать всё"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:446
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:499
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:556
|
||||
msgid "Size"
|
||||
msgstr "Размер"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:932
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1169
|
||||
msgid "Something with that name is already here, and nothing is overwritten."
|
||||
msgstr "Здесь уже есть объект с таким именем; ничего не перезаписывается."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Source and destination are the same directory."
|
||||
msgstr "Источник и назначение — один и тот же каталог."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:590
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:748
|
||||
msgid "The file is %.1f MB; this editor opens files up to 1 MB."
|
||||
msgstr "Файл весит %.1f МБ; редактор открывает файлы до 1 МБ."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:532
|
||||
msgid "This directory is empty."
|
||||
msgstr "Каталог пуст."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Tick the files you want first."
|
||||
msgstr "Сначала отметьте нужные файлы."
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "Tiles"
|
||||
msgstr "Плитка"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:555
|
||||
msgid "Type"
|
||||
msgstr "Тип"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1687
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1718
|
||||
msgid "Up"
|
||||
msgstr "Вверх"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1728
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1731
|
||||
msgid "Upload"
|
||||
msgstr "Загрузить"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1037
|
||||
msgid "Uploaded %d of %d…"
|
||||
msgstr "Загружено %d из %d…"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1026
|
||||
msgid "Uploading %d file(s)…"
|
||||
msgstr "Загрузка файлов: %d…"
|
||||
|
||||
#~ msgid "Forget"
|
||||
#~ msgstr "Забыть"
|
||||
@@ -0,0 +1,401 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to copy"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1694
|
||||
msgid "%d item(s) to move"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1279
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1706
|
||||
msgid "%d selected"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:987
|
||||
msgid "%s could not be removed (%s). Delete it and everything inside?"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:978
|
||||
msgid "%s is a directory. Delete it and everything inside?"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:608
|
||||
msgid "%s is not text; opening it as hex."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1168
|
||||
msgid "%s was not moved"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:931
|
||||
msgid "%s was not pasted"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
msgid "A name may not contain a slash."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:449
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:508
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:559
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:864
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:852
|
||||
msgid "Apply to everything inside"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:856
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1696
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:883
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:884
|
||||
msgid "Cannot change %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Cannot change permissions"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1002
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1003
|
||||
msgid "Cannot create directory"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1011
|
||||
msgid "Cannot create file"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:991
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:992
|
||||
msgid "Cannot delete %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:574
|
||||
msgid "Cannot download %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:589
|
||||
msgid "Cannot edit %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1171
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1172
|
||||
msgid "Cannot move %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:747
|
||||
msgid "Cannot open %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:695
|
||||
msgid "Cannot open the editor"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Cannot paste"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:934
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:935
|
||||
msgid "Cannot paste %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:349
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:368
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:696
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:777
|
||||
msgid "Cannot read %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:947
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:951
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:952
|
||||
msgid "Cannot rename %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:807
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:826
|
||||
msgid "Cannot save %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1035
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1038
|
||||
msgid "Cannot upload %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:661
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:769
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1280
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1301
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1708
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1722
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:517
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1283
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1304
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1710
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:976
|
||||
msgid "Delete %d item(s)? This cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1724
|
||||
msgid "Delete selected"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "Directory"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1711
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:511
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1292
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:509
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1783
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:649
|
||||
msgid "Find and replace"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1686
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1717
|
||||
msgid "Go"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "List"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1740
|
||||
msgid "Long press a file for actions"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:448
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:504
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:558
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1281
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1302
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1709
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1723
|
||||
msgid "Move"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1162
|
||||
msgid "Move %d item(s) to %s?"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1163
|
||||
msgid "Moving %d item(s)…"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:440
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:481
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:554
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:999
|
||||
msgid "New directory name:"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1317
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1721
|
||||
msgid "New file"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1007
|
||||
msgid "New file name:"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1316
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1720
|
||||
msgid "New folder"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Nothing selected"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1287
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1291
|
||||
msgid "Open as hex"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:848
|
||||
msgid "Owner (user:group)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1318
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1695
|
||||
msgid "Paste here"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1748
|
||||
msgid "Path"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:447
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:500
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:557
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:844
|
||||
msgid "Permissions (octal)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:874
|
||||
msgid "Permissions must be three or four octal digits, e.g. 644."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:513
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1294
|
||||
msgid "Properties"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:842
|
||||
msgid "Properties of %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1321
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1719
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:515
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1293
|
||||
msgid "Rename"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:945
|
||||
msgid "Rename \"%s\" to:"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:656
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:764
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:805
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:823
|
||||
msgid "Saved."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:788
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:817
|
||||
msgid "Saving…"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:797
|
||||
msgid "Saving… %d%%"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1298
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:487
|
||||
msgid "Select %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1299
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1320
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1707
|
||||
msgid "Select all"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:446
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:499
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:556
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:932
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1169
|
||||
msgid "Something with that name is already here, and nothing is overwritten."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:923
|
||||
msgid "Source and destination are the same directory."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:590
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:748
|
||||
msgid "The file is %.1f MB; this editor opens files up to 1 MB."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:532
|
||||
msgid "This directory is empty."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:894
|
||||
msgid "Tick the files you want first."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1770
|
||||
msgid "Tiles"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:445
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:498
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:555
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1687
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1718
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1728
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1731
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1037
|
||||
msgid "Uploaded %d of %d…"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/footstrap-files/browser.js:1026
|
||||
msgid "Uploading %d file(s)…"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"admin/system/footstrap-files": {
|
||||
"title": "Files",
|
||||
"order": 81,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "footstrap-files/browser"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-footstrap-files"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"luci-app-footstrap-files": {
|
||||
"description": "Browse and change files on the router",
|
||||
"write": {
|
||||
"cgi-io": [ "upload", "download" ],
|
||||
"ubus": {
|
||||
"file": [ "list", "stat", "read", "write", "remove", "exec" ]
|
||||
},
|
||||
"file": {
|
||||
"/*": [ "list", "read", "write", "exec" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# Rescan this package's sources into po/templates/footstrap-files.pot and merge it into every
|
||||
# po/<lang>/footstrap-files.po. Run after adding or changing ANY _('…') string.
|
||||
#
|
||||
# ./update-po.sh rescan, merge, report what is still untranslated
|
||||
# ./update-po.sh --check change nothing; fail if the .pot is stale or a string is untranslated
|
||||
#
|
||||
# A missing translation cannot fail loudly — an uncompiled _() falls through to its English msgid
|
||||
# and renders in English with nothing reporting it — so --check is a gate rather than a suggestion.
|
||||
#
|
||||
# The directory is `po/`, which is what luci.mk's LUCI_LANGUAGES globs and what Weblate translates.
|
||||
# Anything else stops luci.mk emitting the per-language packages.
|
||||
#
|
||||
# Nothing here runs on the buildbot — luci.mk calls po2lmo itself. This needs perl and gettext,
|
||||
# which the OpenWrt build does not have.
|
||||
#
|
||||
# The scanner is LuCI's OWN build/i18n-scan.pl rather than a grep: it understands the `_('x', 'ctx')`
|
||||
# second argument and would cover a .ut template if this package ever grew one. A grep would choke
|
||||
# on the first apostrophe inside a string.
|
||||
#
|
||||
# The scanner is fetched from openwrt/luci at the commit in luci-upstream.pin and CHECKSUMMED
|
||||
# before perl is pointed at it — it is a script downloaded over the network and then executed as
|
||||
# the gate deciding whether the catalogue is complete, so off a moving branch the gate would be
|
||||
# whatever upstream pushed last. A local checkout is preferred when there is one, so a developer's
|
||||
# machine does not need the network; the checksum is verified either way, because a checkout
|
||||
# sitting on a different commit is exactly the drift the pin exists to catch.
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
POT='po/templates/footstrap-files.pot'
|
||||
CHECK=0
|
||||
[ "${1:-}" = '--check' ] && CHECK=1
|
||||
|
||||
for tool in perl xgettext msgmerge msgfmt msginit; do
|
||||
command -v "$tool" >/dev/null || { echo "update-po: $tool not found (install perl + gettext)" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. ./luci-upstream.pin
|
||||
|
||||
fetched=''; fresh=''; old_ids=''; new_ids=''
|
||||
# shellcheck disable=SC2064 # expand nothing now: the names are assigned as the script proceeds
|
||||
trap 'rm -f "$fetched" "$fresh" "$old_ids" "$new_ids"' EXIT INT TERM
|
||||
|
||||
scanner=''
|
||||
for c in "${LUCI_SRC:-}/build/i18n-scan.pl" ../../luci/build/i18n-scan.pl ../../luci-fork/build/i18n-scan.pl; do
|
||||
[ -f "$c" ] && { scanner="$c"; break; }
|
||||
done
|
||||
if [ -z "$scanner" ]; then
|
||||
scanner=$(mktemp); fetched="$scanner"
|
||||
curl -sfL --proto '=https' --proto-redir '=https' \
|
||||
"https://raw.githubusercontent.com/openwrt/luci/$LUCI_PIN/build/i18n-scan.pl" -o "$scanner" || {
|
||||
echo "update-po: cannot fetch i18n-scan.pl — set LUCI_SRC to an openwrt/luci checkout" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
echo "$I18N_SCAN_SHA256 $scanner" | sha256sum -c - >/dev/null 2>&1 || {
|
||||
echo "update-po: i18n-scan.pl does not match luci-upstream.pin ($LUCI_PIN)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p po/templates
|
||||
fresh="$(mktemp)"
|
||||
perl "$scanner" htdocs >"$fresh"
|
||||
|
||||
# Compare the msgids only. The .pot carries a POT-Creation-Date and source line numbers, so a byte
|
||||
# comparison reports every run as a change and the gate becomes noise.
|
||||
ids() { grep '^msgid ' "$1" | sort; }
|
||||
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
[ -f "$POT" ] || { echo "update-po: $POT is missing" >&2; exit 1; }
|
||||
old_ids="$(mktemp)"; new_ids="$(mktemp)"
|
||||
ids "$POT" >"$old_ids"; ids "$fresh" >"$new_ids"
|
||||
diff -u "$old_ids" "$new_ids" || {
|
||||
echo "update-po: $POT is stale — run ./update-po.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
cp "$fresh" "$POT"
|
||||
fi
|
||||
|
||||
rc=0
|
||||
for d in po/*/; do
|
||||
lang="${d%/}"; lang="${lang##*/}"
|
||||
[ "$lang" = 'templates' ] && continue
|
||||
po="$d/footstrap-files.po"
|
||||
if [ "$CHECK" = 0 ]; then
|
||||
if [ -f "$po" ]; then
|
||||
msgmerge --quiet --update --backup=none "$po" "$POT"
|
||||
else
|
||||
msginit --no-translator --locale="$lang" --input="$POT" --output-file="$po"
|
||||
fi
|
||||
fi
|
||||
[ -f "$po" ] || { echo "update-po: $po is missing" >&2; rc=1; continue; }
|
||||
msgfmt --check --statistics -o /dev/null "$po" 2>&1 | sed "s/^/$lang: /"
|
||||
# an empty msgstr is a string that will render in English on a translated UI
|
||||
untranslated=$(msgattrib --untranslated "$po" | grep -c '^msgid "' || true)
|
||||
if [ "$untranslated" -gt 1 ]; then
|
||||
echo "$lang: $((untranslated - 1)) untranslated" >&2
|
||||
[ "$CHECK" = 1 ] && rc=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$rc"
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.16
|
||||
PKG_RELEASE:=290
|
||||
PKG_RELEASE:=291
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -154,7 +154,7 @@ o:depends({ custom = false })
|
||||
o = s:option(FileUpload, "ech_keyFile", translate("Path to the ECH key file"), translate("as:") .. "/etc/ssl/ech.pem")
|
||||
o.validate = function(self, value, t)
|
||||
if value and value ~= "" then
|
||||
if not fs.access(value) then
|
||||
if not api.fs.access(value) then
|
||||
return nil, translate("Can't find this file!")
|
||||
else
|
||||
return value
|
||||
|
||||
Reference in new issue
Block a user