Theseus: bundle the Pithos add-on
Ships Pithos (the s3d control panel) as a built-in extension: the dock menu opens it in a tab served from a loopback port, and "Stop s3d" shuts the gateway down. Generated by Pithos/scripts/build-theseus-addon.mjs. yaml is vendored under vendor/yaml/lib rather than node_modules/ or dist/, because Theseus git-ignores both and a clean-worktree release build would otherwise ship the add-on without its only dependency.
This commit is contained in:
parent
c77489b2c4
commit
e01beb419c
95 changed files with 11407 additions and 0 deletions
373
bundled-addons/pithos/LICENSE
Normal file
373
bundled-addons/pithos/LICENSE
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
27
bundled-addons/pithos/addon.json
Normal file
27
bundled-addons/pithos/addon.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"id": "pithos",
|
||||
"name": "Pithos",
|
||||
"version": "0.1.0",
|
||||
"description": "Run s3d, the Sia S3 gateway, from Theseus: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||
"main": "index.js",
|
||||
"capabilities": [
|
||||
"toolbar-menu"
|
||||
],
|
||||
"toolbar-menu": {
|
||||
"title": "Pithos",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||
"items": [
|
||||
{
|
||||
"id": "open",
|
||||
"label": "Open Pithos"
|
||||
},
|
||||
{
|
||||
"id": "stop",
|
||||
"label": "Stop s3d"
|
||||
}
|
||||
]
|
||||
},
|
||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/pithos/updates.json"
|
||||
}
|
||||
34
bundled-addons/pithos/core/admin.js
Normal file
34
bundled-addons/pithos/core/admin.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// s3d's admin API (Basic auth, empty user): upload stats, flush, snapshots.
|
||||
|
||||
function authHeader(password) {
|
||||
return 'Basic ' + Buffer.from(':' + password).toString('base64');
|
||||
}
|
||||
|
||||
async function adminFetch(cfg, method, route, { timeout = 10_000 } = {}) {
|
||||
if (!cfg.adminAddress || !cfg.adminPassword) throw new Error('adminAddress and adminPassword must be set in s3d.yml');
|
||||
const ctl = new AbortController();
|
||||
const timer = timeout ? setTimeout(() => ctl.abort(), timeout) : null;
|
||||
try {
|
||||
const res = await fetch(`http://${cfg.adminAddress}${route}`, {
|
||||
method, headers: { authorization: authHeader(cfg.adminPassword) }, signal: ctl.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!res.ok) throw new Error(`admin API ${route}: ${res.status} ${text.trim()}`);
|
||||
return text;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadStats(cfg) {
|
||||
return JSON.parse(await adminFetch(cfg, 'GET', '/stats/uploads'));
|
||||
}
|
||||
|
||||
// Blocks until every pending object is on Sia, so no timeout.
|
||||
export async function flush(cfg) {
|
||||
await adminFetch(cfg, 'POST', '/objects/flush', { timeout: 0 });
|
||||
}
|
||||
|
||||
export async function snapshot(cfg) {
|
||||
return adminFetch(cfg, 'POST', '/snapshots', { timeout: 0 });
|
||||
}
|
||||
77
bundled-addons/pithos/core/binary.js
Normal file
77
bundled-addons/pithos/core/binary.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Installs the pinned s3d release into Pithos's own bin folder. The digests are
|
||||
// GitHub's published sha256 for each release asset; a download that does not
|
||||
// match is discarded before anything is written to the bin folder.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import zlib from 'node:zlib';
|
||||
|
||||
export const S3D_VERSION = '0.2.0';
|
||||
|
||||
const ASSETS = {
|
||||
'win32-x64': { file: 's3d_windows_amd64.zip', sha256: '239356ef55228d03aa1889c7208357b83faf66947ade2cba4f7a3893c632c473' },
|
||||
'linux-x64': { file: 's3d_linux_amd64.zip', sha256: 'e537d68be8b6749e3e239d572c497a4f1ebc81ef5a3b70d5ec3cc9ffa6498e73' },
|
||||
'linux-arm64': { file: 's3d_linux_arm64.zip', sha256: '76848764f09922f31489c809514cdc928b3882836d6e5b64dcac085a84b4958b' },
|
||||
'darwin-x64': { file: 's3d_darwin_amd64.zip', sha256: 'd7fe282efab35242b001ed9040d833334034682225b4c2b6b2eda67ecca8992e' },
|
||||
'darwin-arm64': { file: 's3d_darwin_arm64.zip', sha256: '9eb6b73cb30b5b6b54fbbc583af6f7d9e56a30c55247ac70568918a1c98a3353' },
|
||||
};
|
||||
|
||||
export function assetForPlatform() {
|
||||
return ASSETS[`${process.platform}-${process.arch}`] || null;
|
||||
}
|
||||
|
||||
export async function installS3d(binDir, onProgress = () => {}) {
|
||||
const asset = assetForPlatform();
|
||||
if (!asset) throw new Error(`no s3d release for ${process.platform}-${process.arch}`);
|
||||
const url = `https://github.com/SiaFoundation/s3d/releases/download/v${S3D_VERSION}/${asset.file}`;
|
||||
onProgress({ phase: 'download', url });
|
||||
const res = await fetch(url, { redirect: 'follow' });
|
||||
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
|
||||
const zip = Buffer.from(await res.arrayBuffer());
|
||||
const digest = crypto.createHash('sha256').update(zip).digest('hex');
|
||||
if (digest !== asset.sha256) throw new Error(`checksum mismatch for ${asset.file} (got ${digest})`);
|
||||
onProgress({ phase: 'extract' });
|
||||
|
||||
const exe = process.platform === 'win32' ? 's3d.exe' : 's3d';
|
||||
const data = extractFromZip(zip, (name) => path.basename(name) === exe);
|
||||
if (!data) throw new Error(`${exe} not found in ${asset.file}`);
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const target = path.join(binDir, exe);
|
||||
const tmp = target + '.new';
|
||||
fs.writeFileSync(tmp, data, { mode: 0o755 });
|
||||
fs.renameSync(tmp, target);
|
||||
onProgress({ phase: 'done', path: target });
|
||||
return target;
|
||||
}
|
||||
|
||||
// Minimal zip reader: walks the central directory and inflates one entry.
|
||||
function extractFromZip(buf, match) {
|
||||
let eocd = -1;
|
||||
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
|
||||
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
||||
}
|
||||
if (eocd < 0) throw new Error('not a zip file');
|
||||
const count = buf.readUInt16LE(eocd + 10);
|
||||
let p = buf.readUInt32LE(eocd + 16);
|
||||
for (let n = 0; n < count; n++) {
|
||||
if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('corrupt zip directory');
|
||||
const method = buf.readUInt16LE(p + 10);
|
||||
const csize = buf.readUInt32LE(p + 20);
|
||||
const nameLen = buf.readUInt16LE(p + 28);
|
||||
const extraLen = buf.readUInt16LE(p + 30);
|
||||
const commentLen = buf.readUInt16LE(p + 32);
|
||||
const local = buf.readUInt32LE(p + 42);
|
||||
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
|
||||
p += 46 + nameLen + extraLen + commentLen;
|
||||
if (!match(name)) continue;
|
||||
const lName = buf.readUInt16LE(local + 26);
|
||||
const lExtra = buf.readUInt16LE(local + 28);
|
||||
const start = local + 30 + lName + lExtra;
|
||||
const raw = buf.subarray(start, start + csize);
|
||||
if (method === 0) return Buffer.from(raw);
|
||||
if (method === 8) return zlib.inflateRawSync(raw);
|
||||
throw new Error(`unsupported zip method ${method}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
93
bundled-addons/pithos/core/cli.js
Normal file
93
bundled-addons/pithos/core/cli.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Typed wrappers over s3d's user/key subcommands. s3d has no HTTP API for
|
||||
// these; the CLI opens the same SQLite file the daemon uses (WAL mode, so it
|
||||
// is safe while the daemon is running).
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export class CliError extends Error {}
|
||||
|
||||
function check({ code, stdout, stderr }) {
|
||||
if (code !== 0) throw new CliError((stderr || stdout).trim() || `s3d exited with code ${code}`);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
export function makeCli(daemon) {
|
||||
return {
|
||||
// Whether this s3d holds an app key. `s3d login` reports an existing
|
||||
// registration before it reads any input; with stdin closed an
|
||||
// unregistered instance hits EOF at the first prompt and exits.
|
||||
async registration() {
|
||||
const { stdout } = await new Promise((resolve, reject) => {
|
||||
const child = spawn(daemon.binary, ['login'], {
|
||||
cwd: daemon.cwd(), env: daemon.env(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
});
|
||||
let out = '';
|
||||
child.stdout.on('data', (d) => { out += d; });
|
||||
child.stderr.on('data', (d) => { out += d; });
|
||||
const timer = setTimeout(() => child.kill(), 15_000);
|
||||
child.on('error', reject);
|
||||
child.on('exit', () => { clearTimeout(timer); resolve({ stdout: out.replace(/\x1b\[[0-9;]*m/g, '') }); });
|
||||
});
|
||||
const m = stdout.match(/already registered with (\S+?)\.?\s*$/m);
|
||||
return m ? { registered: true, indexerUrl: m[1] } : { registered: false };
|
||||
},
|
||||
|
||||
async version() {
|
||||
const out = check(await daemon.run(['version']));
|
||||
const m = out.match(/s3d v?(\S+)/);
|
||||
const commit = out.match(/Commit:\s*(\S+)/);
|
||||
return { version: m ? m[1] : null, commit: commit ? commit[1] : null, raw: out.trim() };
|
||||
},
|
||||
|
||||
async listUsers() {
|
||||
const out = check(await daemon.run(['users', 'list']));
|
||||
if (/^No users found/m.test(out)) return [];
|
||||
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
||||
},
|
||||
|
||||
async createUser(name) {
|
||||
validateName(name);
|
||||
check(await daemon.run(['users', 'create', name]));
|
||||
},
|
||||
|
||||
async deleteUser(name) {
|
||||
validateName(name);
|
||||
check(await daemon.run(['users', 'delete', name]));
|
||||
},
|
||||
|
||||
// [{ accessKeyId, secretKey, user }]
|
||||
async listKeys(user) {
|
||||
const out = check(await daemon.run(user ? ['keys', 'list', user] : ['keys', 'list']));
|
||||
if (/^No access keys found/m.test(out)) return [];
|
||||
return out.split(/\r?\n/).filter(Boolean).map((line) => {
|
||||
const [accessKeyId, secretKey, ...rest] = line.split('\t');
|
||||
return { accessKeyId, secretKey, user: rest.join('\t') };
|
||||
});
|
||||
},
|
||||
|
||||
async createKey(user, { accessKey, secretKey } = {}) {
|
||||
validateName(user);
|
||||
const args = ['keys', 'create'];
|
||||
if (accessKey || secretKey) args.push('--access-key', accessKey || '', '--secret-key', secretKey || '');
|
||||
args.push(user);
|
||||
const out = check(await daemon.run(args));
|
||||
const ak = out.match(/Access Key:\s*(\S+)/);
|
||||
const sk = out.match(/Secret Key:\s*(\S+)/);
|
||||
if (!ak || !sk) throw new CliError(`unexpected output from s3d keys create: ${out.trim()}`);
|
||||
return { accessKeyId: ak[1], secretKey: sk[1], user };
|
||||
},
|
||||
|
||||
async deleteKey(accessKeyId) {
|
||||
if (!/^[\x21-\x7e]{1,128}$/.test(accessKeyId)) throw new CliError('invalid access key id');
|
||||
check(await daemon.run(['keys', 'delete', accessKeyId]));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Users are passed as argv, never through a shell, but a leading '-' would be
|
||||
// read as a flag by s3d's parser.
|
||||
function validateName(name) {
|
||||
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$/.test(name)) {
|
||||
throw new CliError('user names are 1-64 characters: letters, digits, . _ @ -, not starting with a symbol');
|
||||
}
|
||||
}
|
||||
79
bundled-addons/pithos/core/config.js
Normal file
79
bundled-addons/pithos/core/config.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// s3d.yml read/write. Edits go through the yaml Document API so keys the GUI
|
||||
// does not know about (and the operator's comments) survive a save.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import YAML from '../vendor/yaml/lib/index.js';
|
||||
import { defaultDataDir } from './paths.js';
|
||||
|
||||
// Fields the settings form exposes, as dotted yaml paths. Anything else in the
|
||||
// file is left untouched.
|
||||
export const EDITABLE = [
|
||||
'apiAddress', 'apiHTTPSAddress', 'adminAddress', 'adminPassword', 'directory',
|
||||
'sia.diskUsageLimit', 'sia.uploadThreads', 'sia.dataShards', 'sia.parityShards',
|
||||
's3.hostBases',
|
||||
'log.stdout.enabled', 'log.stdout.level', 'log.file.enabled', 'log.file.level',
|
||||
];
|
||||
|
||||
export const DEFAULTS = {
|
||||
apiAddress: '127.0.0.1:8000',
|
||||
adminAddress: '127.0.0.1:8001',
|
||||
};
|
||||
|
||||
export function readConfig(file) {
|
||||
const exists = fs.existsSync(file);
|
||||
const doc = exists ? YAML.parseDocument(fs.readFileSync(file, 'utf8')) : new YAML.Document({});
|
||||
if (doc.errors.length) throw new Error(`${file}: ${doc.errors[0].message}`);
|
||||
const data = doc.toJS() || {};
|
||||
return {
|
||||
file,
|
||||
exists,
|
||||
data,
|
||||
// Effective values the daemon will use, for things the GUI needs to reach it.
|
||||
apiAddress: data.apiAddress || DEFAULTS.apiAddress,
|
||||
adminAddress: data.adminAddress || DEFAULTS.adminAddress,
|
||||
adminPassword: data.adminPassword || '',
|
||||
directory: process.env.S3D_DATA_DIR || data.directory || defaultDataDir(),
|
||||
};
|
||||
}
|
||||
|
||||
// patch: { 'sia.uploadThreads': 8, 'apiHTTPSAddress': null, ... }; null/'' deletes the key.
|
||||
export function writeConfig(file, patch) {
|
||||
const exists = fs.existsSync(file);
|
||||
const doc = exists ? YAML.parseDocument(fs.readFileSync(file, 'utf8')) : new YAML.Document({});
|
||||
if (doc.contents == null) doc.contents = doc.createNode({});
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (!EDITABLE.includes(key)) throw new Error(`not an editable setting: ${key}`);
|
||||
const p = key.split('.');
|
||||
if (value === null || value === '' || (Array.isArray(value) && value.length === 0)) doc.deleteIn(p);
|
||||
else doc.setIn(p, value);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const tmp = file + '.tmp';
|
||||
fs.writeFileSync(tmp, doc.toString(), { mode: 0o600 });
|
||||
fs.renameSync(tmp, file);
|
||||
return readConfig(file);
|
||||
}
|
||||
|
||||
// A fresh config the daemon can start with: admin API on, random password.
|
||||
export function ensureConfig(file) {
|
||||
if (fs.existsSync(file)) {
|
||||
const cfg = readConfig(file);
|
||||
if (cfg.data.adminAddress && cfg.data.adminPassword) return cfg;
|
||||
// s3d refuses to start without both, and the GUI needs them for stats.
|
||||
return writeConfig(file, {
|
||||
adminAddress: cfg.data.adminAddress || DEFAULTS.adminAddress,
|
||||
adminPassword: cfg.data.adminPassword || randomPassword(),
|
||||
});
|
||||
}
|
||||
return writeConfig(file, {
|
||||
apiAddress: DEFAULTS.apiAddress,
|
||||
adminAddress: DEFAULTS.adminAddress,
|
||||
adminPassword: randomPassword(),
|
||||
});
|
||||
}
|
||||
|
||||
export function randomPassword() {
|
||||
return crypto.randomBytes(18).toString('base64url');
|
||||
}
|
||||
117
bundled-addons/pithos/core/daemon.js
Normal file
117
bundled-addons/pithos/core/daemon.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// Owns the s3d process: start/stop, a log ring buffer, and one-shot CLI runs
|
||||
// (users/keys/version) against the same config file the daemon uses.
|
||||
|
||||
import { spawn, execFile } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import path from 'node:path';
|
||||
|
||||
const LOG_LINES = 2000;
|
||||
const ANSI = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
export class Daemon extends EventEmitter {
|
||||
constructor({ binary, configFile }) {
|
||||
super();
|
||||
this.binary = binary;
|
||||
this.configFile = configFile;
|
||||
this.child = null;
|
||||
this.state = 'stopped'; // stopped | starting | running | stopping | crashed
|
||||
this.exitCode = null;
|
||||
this.startedAt = null;
|
||||
this.logs = [];
|
||||
this.seq = 0;
|
||||
}
|
||||
|
||||
env() {
|
||||
// S3D_CONFIG_FILE pins every invocation to the config this GUI edits; the
|
||||
// cwd is the config's folder because s3d also searches ./s3d.yml first.
|
||||
return { ...process.env, S3D_CONFIG_FILE: this.configFile };
|
||||
}
|
||||
|
||||
cwd() { return path.dirname(this.configFile); }
|
||||
|
||||
status() {
|
||||
return {
|
||||
state: this.state,
|
||||
pid: this.child?.pid ?? null,
|
||||
exitCode: this.exitCode,
|
||||
startedAt: this.startedAt,
|
||||
binary: this.binary,
|
||||
needsLogin: !!this.needsLogin,
|
||||
configFile: this.configFile,
|
||||
};
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
this.state = state;
|
||||
this.emit('status', this.status());
|
||||
}
|
||||
|
||||
pushLog(stream, chunk) {
|
||||
for (const raw of chunk.toString('utf8').split(/\r?\n/)) {
|
||||
const line = raw.replace(ANSI, '');
|
||||
if (!line) continue;
|
||||
const entry = { seq: ++this.seq, t: Date.now(), stream, line };
|
||||
// s3d logs "server started" once every listener is up.
|
||||
if (this.state === 'starting' && /server started/.test(line)) this.setState('running');
|
||||
if (/No app key found/.test(line)) this.needsLogin = true;
|
||||
this.logs.push(entry);
|
||||
if (this.logs.length > LOG_LINES) this.logs.shift();
|
||||
this.emit('log', entry);
|
||||
}
|
||||
}
|
||||
|
||||
logsSince(seq = 0) { return this.logs.filter((l) => l.seq > seq); }
|
||||
|
||||
start() {
|
||||
if (!this.binary) throw new Error('s3d binary not found');
|
||||
if (this.child) return this.status();
|
||||
this.exitCode = null;
|
||||
this.needsLogin = false;
|
||||
this.setState('starting');
|
||||
const child = spawn(this.binary, [], {
|
||||
cwd: this.cwd(), env: this.env(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
});
|
||||
this.child = child;
|
||||
this.startedAt = Date.now();
|
||||
child.stdout.on('data', (d) => this.pushLog('out', d));
|
||||
child.stderr.on('data', (d) => this.pushLog('err', d));
|
||||
child.on('error', (err) => {
|
||||
this.pushLog('err', `failed to start s3d: ${err.message}`);
|
||||
// A spawn failure never emits 'exit'.
|
||||
if (child.pid === undefined) { this.child = null; this.setState('crashed'); }
|
||||
});
|
||||
child.on('exit', (code, signal) => {
|
||||
this.child = null;
|
||||
this.exitCode = code;
|
||||
this.pushLog('sys', `s3d exited (${signal || `code ${code}`})`);
|
||||
this.setState(this.state === 'stopping' ? 'stopped' : 'crashed');
|
||||
});
|
||||
return this.status();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
const child = this.child;
|
||||
if (!child) return this.status();
|
||||
this.setState('stopping');
|
||||
const exited = new Promise((r) => child.once('exit', r));
|
||||
child.kill(); // SIGTERM on unix; TerminateProcess on Windows
|
||||
const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 10_000);
|
||||
await exited;
|
||||
clearTimeout(timer);
|
||||
return this.status();
|
||||
}
|
||||
|
||||
// One-shot CLI run. Resolves { code, stdout, stderr }; never rejects on a
|
||||
// non-zero exit so callers can surface s3d's own error text.
|
||||
run(args, { timeout = 30_000 } = {}) {
|
||||
if (!this.binary) return Promise.reject(new Error('s3d binary not found'));
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(this.binary, args, {
|
||||
cwd: this.cwd(), env: this.env(), timeout, windowsHide: true, maxBuffer: 16 << 20,
|
||||
}, (err, stdout, stderr) => {
|
||||
if (err && typeof err.code !== 'number') return reject(err);
|
||||
resolve({ code: err ? err.code : 0, stdout: stdout.replace(ANSI, ''), stderr: stderr.replace(ANSI, '') });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
185
bundled-addons/pithos/core/login.js
Normal file
185
bundled-addons/pithos/core/login.js
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// Drives `s3d login` through a pseudo-terminal. s3d reads the recovery phrase
|
||||
// with term.ReadPassword, which needs a real console, so plain pipes fail.
|
||||
// The phrase only ever lives in this process's memory: it is written to the
|
||||
// pty and never logged or emitted except the one-time display of a phrase
|
||||
// s3d generated, which the user has to copy down.
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07/g;
|
||||
|
||||
let ptyModule;
|
||||
async function loadPty() {
|
||||
if (ptyModule === undefined) {
|
||||
try { ptyModule = await import('@lydell/node-pty'); } catch { ptyModule = null; }
|
||||
}
|
||||
return ptyModule && (ptyModule.default || ptyModule);
|
||||
}
|
||||
|
||||
export class LoginSession extends EventEmitter {
|
||||
// registration: async () => ({ registered, indexerUrl }), used to learn the
|
||||
// outcome when login ran in a console window we cannot read.
|
||||
constructor(daemon, { registration } = {}) {
|
||||
super();
|
||||
this.daemon = daemon;
|
||||
this.registration = registration;
|
||||
this.term = null;
|
||||
this.buffer = '';
|
||||
this.state = 'idle';
|
||||
// idle | indexer | phrase | confirm | approve | registering | done | already | failed
|
||||
this.info = {};
|
||||
}
|
||||
|
||||
snapshot() { return { state: this.state, ...this.info }; }
|
||||
|
||||
set(state, info = {}) {
|
||||
this.state = state;
|
||||
Object.assign(this.info, info);
|
||||
this.emit('state', this.snapshot());
|
||||
}
|
||||
|
||||
async start({ indexerUrl, phrase }) {
|
||||
if (this.term) throw new Error('a login is already in progress');
|
||||
this.answers = { indexerUrl: indexerUrl || '', phrase: phrase || '' };
|
||||
this.info = {};
|
||||
const pty = await loadPty();
|
||||
if (!pty) return this.startInConsole();
|
||||
this.buffer = '';
|
||||
this.generated = null;
|
||||
this.lastError = null;
|
||||
const term = pty.spawn(this.daemon.binary, ['login'], {
|
||||
name: 'xterm', cols: 200, rows: 50, cwd: this.daemon.cwd(), env: this.daemon.env(),
|
||||
});
|
||||
this.term = term;
|
||||
this.set('starting');
|
||||
// A cancelled session's pty can still flush output or exit after a new
|
||||
// one started; only the current term may drive the state machine.
|
||||
term.onData((d) => {
|
||||
if (this.term !== term) return;
|
||||
try { this.onData(d); } catch (e) { this.fail(e.message); }
|
||||
});
|
||||
term.onExit(({ exitCode }) => {
|
||||
if (this.term !== term) return;
|
||||
this.term = null;
|
||||
this.answers = null;
|
||||
if (!['done', 'already'].includes(this.state)) {
|
||||
this.set('failed', { error: this.lastError || `s3d login exited with code ${exitCode}` });
|
||||
}
|
||||
});
|
||||
return this.snapshot();
|
||||
}
|
||||
|
||||
// Fallback when the pty module is not shipped (the Theseus add-on): run
|
||||
// s3d login in its own console window and check the result when it closes.
|
||||
startInConsole() {
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('run "s3d login" in a terminal on this machine, then reload this page');
|
||||
}
|
||||
// detached gives a console app its own visible console window on Windows.
|
||||
const child = spawn(this.daemon.binary, ['login'], {
|
||||
cwd: this.daemon.cwd(), env: this.daemon.env(), detached: true, stdio: 'ignore', windowsHide: false,
|
||||
});
|
||||
this.term = { kill: () => child.kill(), write() {} };
|
||||
const term = this.term;
|
||||
this.answers = null;
|
||||
this.set('terminal');
|
||||
child.on('error', (e) => { if (this.term === term) this.fail(e.message); });
|
||||
child.on('exit', async () => {
|
||||
if (this.term !== term) return;
|
||||
this.term = null;
|
||||
try {
|
||||
const r = await this.registration();
|
||||
if (r.registered) this.set('done', { indexerUrl: r.indexerUrl });
|
||||
else this.set('failed', { error: 'the login window closed before registration finished' });
|
||||
} catch (e) { this.set('failed', { error: e.message }); }
|
||||
});
|
||||
return this.snapshot();
|
||||
}
|
||||
|
||||
// The user confirms they stored a phrase s3d generated; we echo it back.
|
||||
confirmGenerated() {
|
||||
if (this.state !== 'confirm' || !this.generated) throw new Error('nothing to confirm');
|
||||
this.write(this.generated);
|
||||
this.generated = null;
|
||||
this.info.generatedPhrase = undefined;
|
||||
this.set('approve-wait');
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (this.term) { try { this.term.kill(); } catch {} }
|
||||
this.term = null;
|
||||
this.answers = null;
|
||||
this.generated = null;
|
||||
this.set('idle', { error: undefined, approvalUrl: undefined, generatedPhrase: undefined });
|
||||
}
|
||||
|
||||
write(s) { this.term?.write(s + '\r'); }
|
||||
|
||||
onData(chunk) {
|
||||
this.buffer += chunk.replace(ANSI, '');
|
||||
// Work line-by-line, but prompts have no trailing newline, so also look at
|
||||
// the unterminated tail.
|
||||
const text = this.buffer;
|
||||
|
||||
// Checked first: s3d prints the error and re-prompts in the same chunk,
|
||||
// and answering that prompt with a blank would make it generate a phrase.
|
||||
const bad = text.match(/Invalid recovery phrase: (.+)$/m);
|
||||
if (bad) {
|
||||
// s3d would re-prompt forever; stop and let the user retype.
|
||||
return this.fail(`invalid recovery phrase: ${bad[1].trim()}`);
|
||||
}
|
||||
if (/already registered with (\S+?)\.?\s*$/m.test(text)) {
|
||||
const m = text.match(/already registered with (\S+?)\.?\s*$/m);
|
||||
this.buffer = '';
|
||||
return this.set('already', { indexerUrl: m[1] });
|
||||
}
|
||||
if (/Indexer URL \(default: [^)]*\):\s*$/.test(text)) {
|
||||
this.buffer = '';
|
||||
this.set('indexer');
|
||||
return this.write(this.answers.indexerUrl);
|
||||
}
|
||||
if (/Recovery Phrase:\s*(.+)$/m.test(text) && !this.generated) {
|
||||
// s3d generated a phrase because we sent a blank one.
|
||||
this.generated = text.match(/Recovery Phrase:\s*(.+)$/m)[1].trim();
|
||||
}
|
||||
if (/Confirm recovery phrase:\s*$/.test(text)) {
|
||||
this.buffer = '';
|
||||
if (!this.generated) return this.fail('could not read the generated recovery phrase');
|
||||
return this.set('confirm', { generatedPhrase: this.generated });
|
||||
}
|
||||
if (/Enter recovery phrase:\s*$/.test(text)) {
|
||||
this.buffer = '';
|
||||
this.set('phrase');
|
||||
const p = this.answers.phrase;
|
||||
this.answers.phrase = '';
|
||||
return this.write(p);
|
||||
}
|
||||
const url = text.match(/visiting the following URL:\s*(\S+)/);
|
||||
if (url) {
|
||||
this.buffer = text.slice(url.index + url[0].length);
|
||||
return this.set('approve', { approvalUrl: url[1] });
|
||||
}
|
||||
if (/Login successful/.test(text)) {
|
||||
this.buffer = '';
|
||||
return this.set('done');
|
||||
}
|
||||
if (/app connection was declined/.test(text)) {
|
||||
this.buffer = '';
|
||||
return this.fail('the connection was declined on the indexer');
|
||||
}
|
||||
const fatal = text.match(/^(failed to [^\n]+)$/m);
|
||||
if (fatal) this.lastError = fatal[1].trim();
|
||||
// Keep the tail bounded; anything we care about is short.
|
||||
if (this.buffer.length > 8192) this.buffer = this.buffer.slice(-2048);
|
||||
}
|
||||
|
||||
fail(error) {
|
||||
const term = this.term;
|
||||
this.term = null;
|
||||
this.answers = null;
|
||||
this.generated = null;
|
||||
this.set('failed', { error, generatedPhrase: undefined });
|
||||
if (term) { try { term.kill(); } catch {} }
|
||||
}
|
||||
}
|
||||
3
bundled-addons/pithos/core/package.json
Normal file
3
bundled-addons/pithos/core/package.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
49
bundled-addons/pithos/core/paths.js
Normal file
49
bundled-addons/pithos/core/paths.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Where s3d keeps its config and data on each OS, mirroring the lookup in
|
||||
// s3d's cmd/s3d/main.go (configSearchPaths / applicationDirectoryOS) so the
|
||||
// GUI and a hand-run `s3d` always agree on which instance they are touching.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
export function defaultDataDir() {
|
||||
switch (process.platform) {
|
||||
case 'win32': return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 's3d');
|
||||
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support', 's3d');
|
||||
default: return '/var/lib/s3d';
|
||||
}
|
||||
}
|
||||
|
||||
export function configSearchPaths() {
|
||||
const paths = [];
|
||||
if (process.env.S3D_DATA_DIR) paths.push(path.join(process.env.S3D_DATA_DIR, 's3d.yml'));
|
||||
switch (process.platform) {
|
||||
case 'win32': paths.push(path.join(defaultDataDir(), 's3d.yml')); break;
|
||||
case 'darwin': paths.push(path.join(defaultDataDir(), 's3d.yml')); break;
|
||||
default: paths.push('/etc/s3d/s3d.yml', '/var/lib/s3d/s3d.yml');
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
// The config file this GUI manages: an explicit S3D_CONFIG_FILE wins, then the
|
||||
// first existing search path, then the first search path (created on save).
|
||||
export function resolveConfigPath(explicit) {
|
||||
if (explicit) return path.resolve(explicit);
|
||||
if (process.env.S3D_CONFIG_FILE) return path.resolve(process.env.S3D_CONFIG_FILE);
|
||||
const candidates = configSearchPaths();
|
||||
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
||||
}
|
||||
|
||||
const EXE = process.platform === 'win32' ? 's3d.exe' : 's3d';
|
||||
|
||||
// Binary lookup: explicit path, S3D_BIN, the vendored copy next to Pithos, then PATH.
|
||||
export function resolveBinary(explicit, vendorDir) {
|
||||
const tries = [explicit, process.env.S3D_BIN, vendorDir && path.join(vendorDir, EXE)].filter(Boolean);
|
||||
for (const p of tries) if (fs.existsSync(p)) return path.resolve(p);
|
||||
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
||||
if (!dir) continue;
|
||||
const p = path.join(dir, EXE);
|
||||
if (fs.existsSync(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
221
bundled-addons/pithos/core/s3.js
Normal file
221
bundled-addons/pithos/core/s3.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// A small S3 client for s3d: AWS SigV4 (path-style, us-east-1, UNSIGNED-PAYLOAD
|
||||
// for bodies so uploads stream straight through) plus the handful of bucket
|
||||
// and object calls the console needs.
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import http from 'node:http';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
const REGION = 'us-east-1';
|
||||
const SERVICE = 's3';
|
||||
|
||||
const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
||||
const hmac = (k, s) => crypto.createHmac('sha256', k).update(s).digest();
|
||||
|
||||
// RFC 3986 encoding as SigV4 wants it; '/' kept for object keys in paths.
|
||||
function uriEncode(str, keepSlash) {
|
||||
return encodeURIComponent(str)
|
||||
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
|
||||
.replace(keepSlash ? /%2F/g : /$^/, '/');
|
||||
}
|
||||
|
||||
function canonicalQuery(query) {
|
||||
return Object.keys(query).sort()
|
||||
.map((k) => `${uriEncode(k)}=${uriEncode(String(query[k] ?? ''))}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
function amzDate(d = new Date()) {
|
||||
return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||
}
|
||||
|
||||
function signingKey(secret, date) {
|
||||
return hmac(hmac(hmac(hmac('AWS4' + secret, date), REGION), SERVICE), 'aws4_request');
|
||||
}
|
||||
|
||||
export function objectPath(bucket, key) {
|
||||
return '/' + uriEncode(bucket) + (key != null ? '/' + uriEncode(key, true) : '');
|
||||
}
|
||||
|
||||
export class S3Client {
|
||||
// endpoint: 'http://127.0.0.1:8000'
|
||||
constructor({ endpoint, accessKeyId, secretKey }) {
|
||||
this.endpoint = new URL(endpoint);
|
||||
this.accessKeyId = accessKeyId;
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
sign(method, path, query, headers, { now = amzDate(), payloadHash = 'UNSIGNED-PAYLOAD' } = {}) {
|
||||
const date = now.slice(0, 8);
|
||||
const h = { ...headers, host: this.endpoint.host, 'x-amz-date': now, 'x-amz-content-sha256': payloadHash };
|
||||
const names = Object.keys(h).map((k) => k.toLowerCase()).sort();
|
||||
const lower = Object.fromEntries(Object.entries(h).map(([k, v]) => [k.toLowerCase(), String(v).trim()]));
|
||||
const signed = names.join(';');
|
||||
const canonical = [method, path, canonicalQuery(query), names.map((n) => `${n}:${lower[n]}\n`).join(''), signed, payloadHash].join('\n');
|
||||
const scope = `${date}/${REGION}/${SERVICE}/aws4_request`;
|
||||
const toSign = ['AWS4-HMAC-SHA256', now, scope, sha256(canonical)].join('\n');
|
||||
const sig = crypto.createHmac('sha256', signingKey(this.secretKey, date)).update(toSign).digest('hex');
|
||||
lower.authorization = `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${scope}, SignedHeaders=${signed}, Signature=${sig}`;
|
||||
delete lower.host;
|
||||
return lower;
|
||||
}
|
||||
|
||||
// Presigned GET for share links. publicEndpoint lets the link name the
|
||||
// address readers will use, which may differ from the loopback the GUI uses.
|
||||
presign(bucket, key, expiresSec = 3600, publicEndpoint, now = amzDate()) {
|
||||
const base = publicEndpoint ? new URL(publicEndpoint) : this.endpoint;
|
||||
const date = now.slice(0, 8);
|
||||
const scope = `${date}/${REGION}/${SERVICE}/aws4_request`;
|
||||
const path = objectPath(bucket, key);
|
||||
const query = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKeyId}/${scope}`,
|
||||
'X-Amz-Date': now,
|
||||
'X-Amz-Expires': String(Math.min(Math.max(1, expiresSec | 0), 604800)),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
const canonical = ['GET', path, canonicalQuery(query), `host:${base.host}\n`, 'host', 'UNSIGNED-PAYLOAD'].join('\n');
|
||||
const toSign = ['AWS4-HMAC-SHA256', now, scope, sha256(canonical)].join('\n');
|
||||
query['X-Amz-Signature'] = crypto.createHmac('sha256', signingKey(this.secretKey, date)).update(toSign).digest('hex');
|
||||
return `${base.origin}${path}?${canonicalQuery(query)}`;
|
||||
}
|
||||
|
||||
// Low-level request. body: Buffer | string | Readable | undefined.
|
||||
// Resolves the raw http.IncomingMessage so callers can stream it.
|
||||
request(method, path, { query = {}, headers = {}, body } = {}) {
|
||||
const signedHeaders = this.sign(method, path, query, headers);
|
||||
const qs = canonicalQuery(query);
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
protocol: this.endpoint.protocol,
|
||||
hostname: this.endpoint.hostname,
|
||||
port: this.endpoint.port,
|
||||
method,
|
||||
path: path + (qs ? '?' + qs : ''),
|
||||
headers: signedHeaders,
|
||||
}, resolve);
|
||||
req.on('error', reject);
|
||||
if (body instanceof Readable) body.pipe(req);
|
||||
else req.end(body);
|
||||
});
|
||||
}
|
||||
|
||||
async call(method, path, opts) {
|
||||
const res = await this.request(method, path, opts);
|
||||
const text = await readBody(res);
|
||||
if (res.statusCode >= 300) throw s3Error(res.statusCode, text);
|
||||
return { status: res.statusCode, headers: res.headers, text };
|
||||
}
|
||||
|
||||
async listBuckets() {
|
||||
const { text } = await this.call('GET', '/');
|
||||
return blocks(text, 'Bucket').map((b) => ({ name: tag(b, 'Name'), created: tag(b, 'CreationDate') }));
|
||||
}
|
||||
|
||||
createBucket(bucket) { return this.call('PUT', objectPath(bucket)); }
|
||||
deleteBucket(bucket) { return this.call('DELETE', objectPath(bucket)); }
|
||||
|
||||
async listObjects(bucket, { prefix = '', delimiter = '/', token, max = 1000 } = {}) {
|
||||
const query = { 'list-type': '2', prefix, delimiter, 'max-keys': String(max) };
|
||||
if (token) query['continuation-token'] = token;
|
||||
const { text } = await this.call('GET', objectPath(bucket), { query });
|
||||
return {
|
||||
folders: blocks(text, 'CommonPrefixes').map((b) => tag(b, 'Prefix')),
|
||||
objects: blocks(text, 'Contents').map((b) => ({
|
||||
key: tag(b, 'Key'), size: Number(tag(b, 'Size')), modified: tag(b, 'LastModified'), etag: tag(b, 'ETag'),
|
||||
})),
|
||||
truncated: tag(text, 'IsTruncated') === 'true',
|
||||
nextToken: tag(text, 'NextContinuationToken'),
|
||||
};
|
||||
}
|
||||
|
||||
async deleteObject(bucket, key) { return this.call('DELETE', objectPath(bucket, key)); }
|
||||
|
||||
// Deletes every key under prefix. Returns the number deleted.
|
||||
async deletePrefix(bucket, prefix) {
|
||||
let n = 0;
|
||||
let token;
|
||||
do {
|
||||
const page = await this.listObjects(bucket, { prefix, delimiter: '', token });
|
||||
for (const o of page.objects) { await this.deleteObject(bucket, o.key); n++; }
|
||||
token = page.truncated ? page.nextToken : null;
|
||||
} while (token);
|
||||
return n;
|
||||
}
|
||||
|
||||
putObject(bucket, key, body, { contentType, contentLength } = {}) {
|
||||
const headers = {};
|
||||
if (contentType) headers['content-type'] = contentType;
|
||||
if (contentLength != null) headers['content-length'] = String(contentLength);
|
||||
return this.call('PUT', objectPath(bucket, key), { headers, body });
|
||||
}
|
||||
|
||||
getObject(bucket, key, { range } = {}) {
|
||||
return this.request('GET', objectPath(bucket, key), { headers: range ? { range } : {} });
|
||||
}
|
||||
|
||||
async getPolicy(bucket) {
|
||||
const res = await this.request('GET', objectPath(bucket), { query: { policy: '' } });
|
||||
const text = await readBody(res);
|
||||
if (res.statusCode === 404) return null;
|
||||
if (res.statusCode >= 300) throw s3Error(res.statusCode, text);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
putPolicy(bucket, policy) {
|
||||
const body = JSON.stringify(policy);
|
||||
return this.call('PUT', objectPath(bucket), { query: { policy: '' }, body, headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) } });
|
||||
}
|
||||
|
||||
deletePolicy(bucket) { return this.call('DELETE', objectPath(bucket), { query: { policy: '' } }); }
|
||||
}
|
||||
|
||||
// The only policy shape s3d accepts: anonymous read of objects and listing.
|
||||
export function publicReadPolicy(bucket, { list = false } = {}) {
|
||||
const statements = [{
|
||||
Effect: 'Allow', Principal: '*', Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::${bucket}/*`],
|
||||
}];
|
||||
if (list) statements.push({ Effect: 'Allow', Principal: '*', Action: ['s3:ListBucket'], Resource: [`arn:aws:s3:::${bucket}`] });
|
||||
return { Version: '2012-10-17', Statement: statements };
|
||||
}
|
||||
|
||||
export function readBody(res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
res.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export class S3Error extends Error {
|
||||
constructor(status, code, message) { super(message || code); this.status = status; this.code = code; }
|
||||
}
|
||||
|
||||
function s3Error(status, text) {
|
||||
return new S3Error(status, tag(text, 'Code') || `HTTP ${status}`, tag(text, 'Message') || text.slice(0, 200) || `HTTP ${status}`);
|
||||
}
|
||||
|
||||
// S3 responses are flat enough that tag extraction is all we need.
|
||||
function blocks(xml, name) {
|
||||
const re = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, 'g');
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(xml))) out.push(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function tag(xml, name) {
|
||||
const m = xml.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
|
||||
return m ? unescapeXml(m[1]) : null;
|
||||
}
|
||||
|
||||
function unescapeXml(s) {
|
||||
return s.replace(/&(lt|gt|amp|quot|apos|#\d+|#x[0-9a-f]+);/gi, (_, e) => {
|
||||
switch (e.toLowerCase()) {
|
||||
case 'lt': return '<'; case 'gt': return '>'; case 'amp': return '&';
|
||||
case 'quot': return '"'; case 'apos': return "'";
|
||||
default: return String.fromCodePoint(e[1].toLowerCase() === 'x' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10));
|
||||
}
|
||||
});
|
||||
}
|
||||
474
bundled-addons/pithos/core/server.js
Normal file
474
bundled-addons/pithos/core/server.js
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
// The Pithos control server: one JSON API + the static UI. Every host (web
|
||||
// console, desktop window, Theseus tab) runs exactly this and points a view
|
||||
// at it, so features are written once.
|
||||
//
|
||||
// Auth: a session secret. Local hosts pass it in the first URL (?t=...);
|
||||
// the web console can instead accept a password. Either way it is exchanged
|
||||
// for an HttpOnly SameSite=Strict cookie, and mutating requests also need the
|
||||
// x-pithos header so a cross-site form can never drive the API.
|
||||
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveConfigPath, resolveBinary } from './paths.js';
|
||||
import { readConfig, writeConfig, ensureConfig, EDITABLE } from './config.js';
|
||||
import { Daemon } from './daemon.js';
|
||||
import { makeCli, CliError } from './cli.js';
|
||||
import { LoginSession } from './login.js';
|
||||
import { S3Client, S3Error, publicReadPolicy, readBody } from './s3.js';
|
||||
import * as admin from './admin.js';
|
||||
import { installS3d, S3D_VERSION, assetForPlatform } from './binary.js';
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const UI_DIR = path.join(HERE, '..', 'ui');
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
||||
'.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.json': 'application/json',
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
class HttpError extends Error {
|
||||
constructor(status, message) { super(message); this.status = status; }
|
||||
}
|
||||
|
||||
export async function createPithos(opts = {}) {
|
||||
const {
|
||||
host = '127.0.0.1',
|
||||
port = 0,
|
||||
configFile: configOpt,
|
||||
binary: binaryOpt,
|
||||
binDir, // where installS3d puts the pinned release
|
||||
password, // web console: optional login password
|
||||
allowedHosts = [], // extra Host header values (reverse proxy names)
|
||||
hostName = 'web', // 'web' | 'desktop' | 'theseus' — UI tweaks only
|
||||
openExternal, // (url) => void, for hosts that can open the system browser
|
||||
} = opts;
|
||||
|
||||
const configFile = resolveConfigPath(configOpt);
|
||||
const daemon = new Daemon({ configFile, binary: resolveBinary(binaryOpt, binDir) });
|
||||
const cli = makeCli(daemon);
|
||||
const login = new LoginSession(daemon, { registration: () => cli.registration() });
|
||||
const secret = crypto.randomBytes(24).toString('base64url');
|
||||
const sessions = new Set();
|
||||
const subscribers = new Set();
|
||||
let versionCache = null;
|
||||
|
||||
const broadcast = (event, data) => {
|
||||
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
||||
for (const res of subscribers) res.write(msg);
|
||||
};
|
||||
daemon.on('status', (s) => {
|
||||
// A registration done outside Pithos (s3d login by hand) shows up on the next start.
|
||||
if (s.state === 'starting' && !regCache?.registered) regCache = null;
|
||||
broadcast('status', s);
|
||||
});
|
||||
daemon.on('log', (l) => broadcast('log', l));
|
||||
login.on('state', (s) => {
|
||||
if (s.state === 'done' || s.state === 'already') regCache = { registered: true, indexerUrl: s.indexerUrl || regCache?.indexerUrl };
|
||||
broadcast('login', s);
|
||||
});
|
||||
|
||||
// Registration costs an s3d process run, so it is cached and only
|
||||
// recomputed when something that could change it happens.
|
||||
let regCache = null;
|
||||
async function registration() {
|
||||
if (!regCache && daemon.binary) regCache = await cli.registration().catch(() => null);
|
||||
return regCache;
|
||||
}
|
||||
|
||||
function cfg() { return readConfig(configFile); }
|
||||
|
||||
async function version() {
|
||||
if (!daemon.binary) return null;
|
||||
if (versionCache?.binary === daemon.binary) return versionCache.info;
|
||||
try {
|
||||
const info = await cli.version();
|
||||
versionCache = { binary: daemon.binary, info };
|
||||
return info;
|
||||
} catch (e) {
|
||||
return { version: null, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// An s3d we did not start (a service, or one run by hand) that answers on
|
||||
// the configured admin address.
|
||||
async function probeExternal(c) {
|
||||
if (daemon.child || !c.adminPassword) return false;
|
||||
try { await admin.uploadStats(c); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function s3For(user) {
|
||||
const keys = await cli.listKeys(user);
|
||||
if (!keys.length) throw new HttpError(409, `user "${user}" has no access keys yet - create one first`);
|
||||
const c = cfg();
|
||||
return new S3Client({ endpoint: `http://${c.apiAddress}`, accessKeyId: keys[0].accessKeyId, secretKey: keys[0].secretKey });
|
||||
}
|
||||
|
||||
// ---- routing ---------------------------------------------------------
|
||||
|
||||
const routes = [];
|
||||
const route = (method, pattern, handler) => {
|
||||
const keys = [];
|
||||
const re = new RegExp('^' + pattern.replace(/:(\w+)/g, (_, k) => { keys.push(k); return '([^/]+)'; }) + '$');
|
||||
routes.push({ method, re, keys, handler });
|
||||
};
|
||||
|
||||
route('GET', '/api/status', async () => {
|
||||
const c = cfg();
|
||||
const v = await version();
|
||||
return {
|
||||
host: hostName,
|
||||
daemon: daemon.status(),
|
||||
external: await probeExternal(c),
|
||||
s3d: { ...v, pinned: S3D_VERSION, installable: !!assetForPlatform(), outdated: !!(v?.version && cmpVersion(v.version, S3D_VERSION) < 0) },
|
||||
config: { file: c.file, exists: c.exists, apiAddress: c.apiAddress, adminAddress: c.adminAddress, directory: c.directory, https: c.data.apiHTTPSAddress || null },
|
||||
login: login.snapshot(),
|
||||
registration: await registration(),
|
||||
};
|
||||
});
|
||||
|
||||
route('POST', '/api/daemon/start', async () => {
|
||||
ensureConfig(configFile);
|
||||
return daemon.start();
|
||||
});
|
||||
route('POST', '/api/daemon/stop', () => daemon.stop());
|
||||
route('POST', '/api/daemon/restart', async () => {
|
||||
await daemon.stop();
|
||||
ensureConfig(configFile);
|
||||
return daemon.start();
|
||||
});
|
||||
route('GET', '/api/logs', (req, url) => daemon.logsSince(Number(url.searchParams.get('since') || 0)));
|
||||
|
||||
route('POST', '/api/binary/install', async () => {
|
||||
if (daemon.child) throw new HttpError(409, 'stop s3d before replacing its binary');
|
||||
if (!binDir) throw new HttpError(400, 'this host has no writable bin folder');
|
||||
const target = await installS3d(binDir, (p) => broadcast('install', p));
|
||||
daemon.binary = target;
|
||||
versionCache = null;
|
||||
regCache = null;
|
||||
return { binary: target, version: await version() };
|
||||
});
|
||||
|
||||
route('GET', '/api/config', () => {
|
||||
const c = cfg();
|
||||
const values = {};
|
||||
for (const k of EDITABLE) values[k] = getIn(c.data, k.split('.')) ?? null;
|
||||
return { file: c.file, exists: c.exists, values };
|
||||
});
|
||||
route('PUT', '/api/config', async (req) => {
|
||||
const patch = await jsonBody(req);
|
||||
const c = writeConfig(configFile, patch);
|
||||
if ('directory' in patch) regCache = null;
|
||||
return { file: c.file, restartNeeded: !!daemon.child };
|
||||
});
|
||||
|
||||
// Login (indexer registration)
|
||||
route('GET', '/api/login', () => login.snapshot());
|
||||
route('POST', '/api/login', async (req) => {
|
||||
const { indexerUrl, phrase } = await jsonBody(req);
|
||||
if (phrase && phrase.trim().split(/\s+/).length !== 12) throw new HttpError(400, 'a recovery phrase is 12 words');
|
||||
if (indexerUrl && !/^https?:\/\/[^\s]+$/.test(indexerUrl)) throw new HttpError(400, 'indexer URL must be http(s)');
|
||||
ensureConfig(configFile);
|
||||
return login.start({ indexerUrl: indexerUrl?.trim(), phrase: phrase?.trim().toLowerCase().split(/\s+/).join(' ') });
|
||||
});
|
||||
route('POST', '/api/login/confirm', () => { login.confirmGenerated(); return login.snapshot(); });
|
||||
route('POST', '/api/login/cancel', () => { login.cancel(); return login.snapshot(); });
|
||||
route('POST', '/api/open-external', async (req) => {
|
||||
const { url } = await jsonBody(req);
|
||||
if (!openExternal) throw new HttpError(400, 'not supported by this host');
|
||||
if (!/^https:\/\//.test(url)) throw new HttpError(400, 'only https links');
|
||||
openExternal(url);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// Users & keys
|
||||
route('GET', '/api/users', async () => {
|
||||
const [users, keys] = await Promise.all([cli.listUsers(), cli.listKeys()]);
|
||||
return users.map((name) => ({ name, keys: keys.filter((k) => k.user === name).map((k) => k.accessKeyId) }));
|
||||
});
|
||||
route('POST', '/api/users', async (req) => {
|
||||
const { name, withKey = true } = await jsonBody(req);
|
||||
await cli.createUser(name);
|
||||
const key = withKey ? await cli.createKey(name) : null;
|
||||
return { name, key };
|
||||
});
|
||||
route('DELETE', '/api/users/:name', async (req, url, p) => { await cli.deleteUser(p.name); return { ok: true }; });
|
||||
route('GET', '/api/keys', (req, url) => cli.listKeys(url.searchParams.get('user') || undefined));
|
||||
route('POST', '/api/keys', async (req) => {
|
||||
const { user, accessKey, secretKey } = await jsonBody(req);
|
||||
return cli.createKey(user, { accessKey, secretKey });
|
||||
});
|
||||
route('DELETE', '/api/keys/:id', async (req, url, p) => { await cli.deleteKey(p.id); return { ok: true }; });
|
||||
|
||||
// Upload pipeline
|
||||
route('GET', '/api/stats', () => admin.uploadStats(cfg()));
|
||||
route('POST', '/api/flush', async () => { await admin.flush(cfg()); return { ok: true }; });
|
||||
|
||||
// Buckets & objects, acting as one s3d user.
|
||||
route('GET', '/api/s3/:user/buckets', async (req, url, p) => (await s3For(p.user)).listBuckets());
|
||||
route('POST', '/api/s3/:user/buckets', async (req, url, p) => {
|
||||
const { name } = await jsonBody(req);
|
||||
if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(name || '')) throw new HttpError(400, 'bucket names are 3-63 chars: lowercase letters, digits, dots, hyphens');
|
||||
await (await s3For(p.user)).createBucket(name);
|
||||
return { name };
|
||||
});
|
||||
route('DELETE', '/api/s3/:user/buckets/:bucket', async (req, url, p) => {
|
||||
await (await s3For(p.user)).deleteBucket(p.bucket);
|
||||
return { ok: true };
|
||||
});
|
||||
route('GET', '/api/s3/:user/buckets/:bucket/objects', async (req, url, p) => {
|
||||
const q = url.searchParams;
|
||||
return (await s3For(p.user)).listObjects(p.bucket, { prefix: q.get('prefix') || '', token: q.get('token') || undefined });
|
||||
});
|
||||
route('PUT', '/api/s3/:user/buckets/:bucket/object', async (req, url, p) => {
|
||||
const key = requireKey(url);
|
||||
const len = req.headers['content-length'];
|
||||
if (len == null) throw new HttpError(411, 'Content-Length required');
|
||||
if (Number(len) > 5 * 1024 ** 3) throw new HttpError(413, 'single uploads are limited to 5 GiB');
|
||||
await (await s3For(p.user)).putObject(p.bucket, key, req, { contentType: req.headers['content-type'], contentLength: len });
|
||||
return { key };
|
||||
});
|
||||
route('GET', '/api/s3/:user/buckets/:bucket/object', async (req, url, p, res) => {
|
||||
const key = requireKey(url);
|
||||
const up = await (await s3For(p.user)).getObject(p.bucket, key, { range: req.headers.range });
|
||||
if (up.statusCode >= 300 && up.statusCode !== 304) {
|
||||
throw new HttpError(up.statusCode, (await readBody(up)).match(/<Message>(.*?)<\/Message>/)?.[1] || `HTTP ${up.statusCode}`);
|
||||
}
|
||||
const headers = {};
|
||||
for (const h of ['content-type', 'content-length', 'content-range', 'accept-ranges', 'etag', 'last-modified']) if (up.headers[h]) headers[h] = up.headers[h];
|
||||
const name = key.split('/').pop() || 'download';
|
||||
headers['content-disposition'] = `${url.searchParams.get('inline') ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(name)}`;
|
||||
headers['x-content-type-options'] = 'nosniff';
|
||||
// Objects are user content: never let them run script on this origin.
|
||||
headers['content-security-policy'] = "sandbox; default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'";
|
||||
res.writeHead(up.statusCode, headers);
|
||||
up.pipe(res);
|
||||
return STREAMED;
|
||||
});
|
||||
route('DELETE', '/api/s3/:user/buckets/:bucket/object', async (req, url, p) => {
|
||||
await (await s3For(p.user)).deleteObject(p.bucket, requireKey(url));
|
||||
return { ok: true };
|
||||
});
|
||||
route('DELETE', '/api/s3/:user/buckets/:bucket/prefix', async (req, url, p) => {
|
||||
const prefix = url.searchParams.get('prefix');
|
||||
if (!prefix) throw new HttpError(400, 'prefix required');
|
||||
return { deleted: await (await s3For(p.user)).deletePrefix(p.bucket, prefix) };
|
||||
});
|
||||
route('GET', '/api/s3/:user/buckets/:bucket/presign', async (req, url, p) => {
|
||||
const q = url.searchParams;
|
||||
const client = await s3For(p.user);
|
||||
return { url: client.presign(p.bucket, requireKey(url), Number(q.get('expires') || 3600), q.get('endpoint') || undefined) };
|
||||
});
|
||||
route('GET', '/api/s3/:user/buckets/:bucket/policy', async (req, url, p) => {
|
||||
const policy = await (await s3For(p.user)).getPolicy(p.bucket);
|
||||
return { policy, public: describePolicy(policy) };
|
||||
});
|
||||
route('PUT', '/api/s3/:user/buckets/:bucket/policy', async (req, url, p) => {
|
||||
const { mode } = await jsonBody(req); // 'private' | 'read' | 'read-list'
|
||||
const client = await s3For(p.user);
|
||||
if (mode === 'private') { await client.deletePolicy(p.bucket).catch((e) => { if (e.status !== 404) throw e; }); }
|
||||
else if (mode === 'read' || mode === 'read-list') await client.putPolicy(p.bucket, publicReadPolicy(p.bucket, { list: mode === 'read-list' }));
|
||||
else throw new HttpError(400, 'mode must be private, read or read-list');
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// ---- server ------------------------------------------------------------
|
||||
|
||||
const STREAMED = Symbol('streamed');
|
||||
let boundPort = port;
|
||||
|
||||
function hostAllowed(h) {
|
||||
if (!h) return false;
|
||||
const ok = [`127.0.0.1:${boundPort}`, `localhost:${boundPort}`, `[::1]:${boundPort}`, ...allowedHosts];
|
||||
return ok.includes(h.toLowerCase());
|
||||
}
|
||||
|
||||
function cookieSession(req) {
|
||||
const m = /(?:^|;\s*)pithos=([^;]+)/.exec(req.headers.cookie || '');
|
||||
return m && sessions.has(m[1]) ? m[1] : null;
|
||||
}
|
||||
|
||||
function issueSession(res) {
|
||||
const id = crypto.randomBytes(24).toString('base64url');
|
||||
sessions.add(id);
|
||||
res.setHeader('set-cookie', `pithos=${id}; HttpOnly; SameSite=Strict; Path=/`);
|
||||
return id;
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// DNS-rebinding guard: a page on evil.example resolving to 127.0.0.1 still
|
||||
// sends its own Host header.
|
||||
if (!hostAllowed(req.headers.host)) { res.writeHead(421); return res.end('misdirected request'); }
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
res.setHeader('x-frame-options', 'DENY');
|
||||
res.setHeader('referrer-policy', 'no-referrer');
|
||||
|
||||
try {
|
||||
// Session bootstrap: ?t=<secret> on any page swaps the secret for a cookie.
|
||||
const t = url.searchParams.get('t');
|
||||
if (t && safeEqual(t, secret) && req.method === 'GET' && !url.pathname.startsWith('/api/')) {
|
||||
issueSession(res);
|
||||
res.writeHead(302, { location: url.pathname });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/session') {
|
||||
if (req.method === 'GET') return sendJson(res, 200, { authenticated: !!cookieSession(req), passwordLogin: !!password, host: hostName });
|
||||
if (req.method === 'POST') {
|
||||
requireCsrf(req);
|
||||
const body = await jsonBody(req);
|
||||
if (!password || !safeEqual(String(body.password || ''), password)) {
|
||||
await new Promise((r) => setTimeout(r, 750));
|
||||
throw new HttpError(401, 'wrong password');
|
||||
}
|
||||
issueSession(res);
|
||||
return sendJson(res, 200, { authenticated: true });
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
const s = cookieSession(req);
|
||||
if (s) sessions.delete(s);
|
||||
res.setHeader('set-cookie', 'pithos=; Max-Age=0; Path=/');
|
||||
return sendJson(res, 200, { authenticated: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
if (!cookieSession(req)) throw new HttpError(401, 'not signed in');
|
||||
if (req.method !== 'GET') requireCsrf(req);
|
||||
|
||||
if (url.pathname === '/api/events') {
|
||||
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive' });
|
||||
res.write(`event: status\ndata: ${JSON.stringify(daemon.status())}\n\n`);
|
||||
res.write(`event: login\ndata: ${JSON.stringify(login.snapshot())}\n\n`);
|
||||
subscribers.add(res);
|
||||
const ping = setInterval(() => res.write(': ping\n\n'), 25_000);
|
||||
req.on('close', () => { clearInterval(ping); subscribers.delete(res); });
|
||||
return;
|
||||
}
|
||||
|
||||
for (const r of routes) {
|
||||
if (r.method !== req.method) continue;
|
||||
const m = r.re.exec(url.pathname);
|
||||
if (!m) continue;
|
||||
const params = Object.fromEntries(r.keys.map((k, i) => [k, decodeURIComponent(m[i + 1])]));
|
||||
const out = await r.handler(req, url, params, res);
|
||||
if (out === STREAMED) return;
|
||||
return sendJson(res, 200, out ?? { ok: true });
|
||||
}
|
||||
throw new HttpError(404, 'no such endpoint');
|
||||
}
|
||||
|
||||
return serveStatic(req, res, url);
|
||||
} catch (err) {
|
||||
const status = err instanceof HttpError ? err.status
|
||||
: err instanceof S3Error ? (err.status >= 400 && err.status < 600 ? err.status : 502)
|
||||
: err instanceof CliError ? 400
|
||||
: 500;
|
||||
if (status === 500) console.error('[pithos]', err);
|
||||
if (res.headersSent) return res.destroy();
|
||||
sendJson(res, status, { error: err.message, code: err.code });
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, host, resolve);
|
||||
});
|
||||
boundPort = server.address().port;
|
||||
const displayHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
|
||||
const baseUrl = `http://${displayHost.includes(':') ? `[${displayHost}]` : displayHost}:${boundPort}/`;
|
||||
|
||||
return {
|
||||
server,
|
||||
daemon,
|
||||
url: baseUrl,
|
||||
// Opening this URL signs the view in.
|
||||
authUrl: `${baseUrl}?t=${secret}`,
|
||||
async close() {
|
||||
login.cancel();
|
||||
for (const s of subscribers) s.end();
|
||||
await daemon.stop();
|
||||
await new Promise((r) => server.close(r));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---- helpers -------------------------------------------------------------
|
||||
|
||||
function sendJson(res, status, body) {
|
||||
const data = JSON.stringify(body);
|
||||
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
||||
res.end(data);
|
||||
}
|
||||
|
||||
function requireCsrf(req) {
|
||||
if (req.headers['x-pithos'] !== '1') throw new HttpError(403, 'missing x-pithos header');
|
||||
}
|
||||
|
||||
function requireKey(url) {
|
||||
const key = url.searchParams.get('key');
|
||||
if (!key) throw new HttpError(400, 'key required');
|
||||
return key;
|
||||
}
|
||||
|
||||
async function jsonBody(req) {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
for await (const c of req) {
|
||||
size += c.length;
|
||||
if (size > 1 << 20) throw new HttpError(413, 'body too large');
|
||||
chunks.push(c);
|
||||
}
|
||||
if (!chunks.length) return {};
|
||||
try { return JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw new HttpError(400, 'invalid JSON'); }
|
||||
}
|
||||
|
||||
function safeEqual(a, b) {
|
||||
const x = Buffer.from(String(a));
|
||||
const y = Buffer.from(String(b));
|
||||
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
||||
}
|
||||
|
||||
function getIn(obj, keys) {
|
||||
return keys.reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
||||
}
|
||||
|
||||
function cmpVersion(a, b) {
|
||||
const pa = a.replace(/^v/, '').split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
||||
const pb = b.replace(/^v/, '').split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function describePolicy(policy) {
|
||||
if (!policy) return 'private';
|
||||
const actions = new Set();
|
||||
for (const s of [].concat(policy.Statement || [])) {
|
||||
if (s.Effect !== 'Allow') continue;
|
||||
if (s.Principal !== '*' && s.Principal?.AWS !== '*') continue;
|
||||
for (const a of [].concat(s.Action || [])) actions.add(a);
|
||||
}
|
||||
if (actions.has('s3:ListBucket')) return 'read-list';
|
||||
if (actions.has('s3:GetObject')) return 'read';
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
function serveStatic(req, res, url) {
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405); return res.end(); }
|
||||
let rel = decodeURIComponent(url.pathname);
|
||||
if (rel === '/' || !path.extname(rel)) rel = '/index.html';
|
||||
const file = path.join(UI_DIR, path.normalize(rel));
|
||||
if (!file.startsWith(UI_DIR + path.sep)) { res.writeHead(403); return res.end(); }
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) { res.writeHead(404); return res.end('not found'); }
|
||||
res.writeHead(200, {
|
||||
'content-type': MIME[path.extname(file)] || 'application/octet-stream',
|
||||
'cache-control': 'no-cache',
|
||||
'content-security-policy': "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'",
|
||||
});
|
||||
res.end(req.method === 'HEAD' ? undefined : data);
|
||||
});
|
||||
}
|
||||
57
bundled-addons/pithos/index.js
Normal file
57
bundled-addons/pithos/index.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Pithos for Theseus. The add-on runs the same control server as the web
|
||||
// console and desktop app, in Theseus's main process, and opens its UI in a
|
||||
// normal tab at a random loopback port. The first URL carries a one-time
|
||||
// secret that the server swaps for an HttpOnly cookie.
|
||||
//
|
||||
// Loaded with require() by the add-on host; core/ is ESM, hence import().
|
||||
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
let pithos = null;
|
||||
let starting = null;
|
||||
|
||||
module.exports = {
|
||||
activate(api) {
|
||||
// The pinned s3d release lands in the per-add-on data folder, never in the
|
||||
// add-on folder itself (that one is replaced on update).
|
||||
const dataDir = path.join(api.dataDir || path.join(api.folder, "..", "..", "extensions-data"), "pithos");
|
||||
|
||||
async function ensureServer() {
|
||||
if (pithos) return pithos;
|
||||
if (!starting) {
|
||||
starting = (async () => {
|
||||
const { createPithos } = await import(pathToFileURL(path.join(__dirname, "core", "server.js")).href);
|
||||
pithos = await createPithos({
|
||||
host: "127.0.0.1",
|
||||
port: 0,
|
||||
hostName: "theseus",
|
||||
binDir: path.join(dataDir, "bin"),
|
||||
openExternal: (url) => api.openTab(url),
|
||||
});
|
||||
api.log(`control server on ${pithos.url}`);
|
||||
return pithos;
|
||||
})().finally(() => { starting = null; });
|
||||
}
|
||||
return starting;
|
||||
}
|
||||
|
||||
api.onMessage("menu-select", async (payload) => {
|
||||
const id = payload && payload.id;
|
||||
if (id === "open") {
|
||||
const p = await ensureServer();
|
||||
api.openTab(p.authUrl);
|
||||
} else if (id === "stop") {
|
||||
if (pithos) await pithos.daemon.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// The host has no quit hook for add-ons; without this an s3d we started
|
||||
// would outlive Theseus.
|
||||
process.on("exit", () => { try { pithos && pithos.daemon.child && pithos.daemon.child.kill(); } catch {} });
|
||||
},
|
||||
|
||||
async deactivate() {
|
||||
if (pithos) { const p = pithos; pithos = null; await p.close(); }
|
||||
},
|
||||
};
|
||||
4
bundled-addons/pithos/package.json
Normal file
4
bundled-addons/pithos/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"type": "commonjs",
|
||||
"private": true
|
||||
}
|
||||
211
bundled-addons/pithos/ui/app.css
Normal file
211
bundled-addons/pithos/ui/app.css
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
:root {
|
||||
--bg: #f6f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f0f2f4;
|
||||
--border: #dfe3e7;
|
||||
--text: #15191d;
|
||||
--muted: #5d6873;
|
||||
--accent: #1e9e72;
|
||||
--accent-ink: #ffffff;
|
||||
--accent-soft: #e2f5ed;
|
||||
--danger: #c93a3a;
|
||||
--danger-soft: #fbe9e9;
|
||||
--warn: #b7791f;
|
||||
--warn-soft: #fdf3e1;
|
||||
--mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
|
||||
--radius: 10px;
|
||||
color-scheme: light;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f1214;
|
||||
--surface: #171b1f;
|
||||
--surface-2: #1e2328;
|
||||
--border: #2a3036;
|
||||
--text: #e6eaee;
|
||||
--muted: #8d98a3;
|
||||
--accent: #3ddc97;
|
||||
--accent-ink: #06231a;
|
||||
--accent-soft: #123327;
|
||||
--danger: #ef6b6b;
|
||||
--danger-soft: #3a1c1c;
|
||||
--warn: #e0a84a;
|
||||
--warn-soft: #3a2c14;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body {
|
||||
background: var(--bg); color: var(--text);
|
||||
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
[hidden] { display: none !important; }
|
||||
a { color: var(--accent); }
|
||||
h1 { font-size: 22px; margin: 0 0 4px; font-weight: 650; }
|
||||
h2 { font-size: 16px; margin: 0 0 12px; font-weight: 600; }
|
||||
h3 { font-size: 14px; margin: 0 0 8px; font-weight: 600; }
|
||||
code, .mono { font-family: var(--mono); font-size: 12.5px; }
|
||||
.muted { color: var(--muted); }
|
||||
.error { color: var(--danger); }
|
||||
.small { font-size: 12.5px; }
|
||||
|
||||
/* layout */
|
||||
.app { display: grid; grid-template-columns: 220px 1fr; min-height: 100%; }
|
||||
.sidebar {
|
||||
background: var(--surface); border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; padding: 16px 12px; gap: 16px;
|
||||
position: sticky; top: 0; height: 100vh;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 17px; padding: 0 8px; }
|
||||
nav { display: flex; flex-direction: column; gap: 2px; }
|
||||
nav a {
|
||||
color: var(--text); text-decoration: none; padding: 8px 10px; border-radius: 8px;
|
||||
}
|
||||
nav a:hover { background: var(--surface-2); }
|
||||
nav a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
||||
.daemon-pill {
|
||||
margin-top: auto; display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px; border-radius: 8px; background: var(--surface-2); font-size: 13px;
|
||||
}
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
|
||||
.dot.running { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
||||
.dot.starting, .dot.stopping { background: var(--warn); }
|
||||
.dot.crashed { background: var(--danger); }
|
||||
|
||||
main { padding: 28px 32px 48px; max-width: 1100px; width: 100%; }
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.page-head p { margin: 0; }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid.two { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
||||
.card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.card + .card { margin-top: 16px; }
|
||||
.grid > .card + .card { margin-top: 0; }
|
||||
|
||||
.row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.row.spread { justify-content: space-between; }
|
||||
.stack { display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
dl.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
||||
dl.kv dt { color: var(--muted); }
|
||||
dl.kv dd { margin: 0; overflow-wrap: anywhere; }
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
|
||||
.stat { background: var(--surface-2); border-radius: 8px; padding: 12px 14px; }
|
||||
.stat .v { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
||||
.stat .l { color: var(--muted); font-size: 12.5px; }
|
||||
.stat.bad .v { color: var(--danger); }
|
||||
|
||||
/* controls */
|
||||
.btn {
|
||||
appearance: none; border: 1px solid var(--border); background: var(--surface);
|
||||
color: var(--text); padding: 7px 14px; border-radius: 8px; font: inherit; cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
|
||||
}
|
||||
.btn:hover:not(:disabled) { background: var(--surface-2); }
|
||||
.btn:disabled { opacity: .5; cursor: default; }
|
||||
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
|
||||
.btn.primary:hover:not(:disabled) { filter: brightness(1.06); background: var(--accent); }
|
||||
.btn.danger { color: var(--danger); }
|
||||
.btn.danger:hover:not(:disabled) { background: var(--danger-soft); }
|
||||
.btn.small { padding: 3px 9px; font-size: 12.5px; }
|
||||
.btn.link { border: none; background: none; color: var(--accent); padding: 0; }
|
||||
|
||||
input[type=text], input[type=password], input[type=number], input[type=url], select, textarea {
|
||||
font: inherit; color: var(--text); background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 7px 10px; width: 100%;
|
||||
}
|
||||
textarea { resize: vertical; min-height: 70px; }
|
||||
input:focus, select:focus, textarea:focus, .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
label.field { display: flex; flex-direction: column; gap: 4px; }
|
||||
label.field > span { font-weight: 550; font-size: 13px; }
|
||||
label.field > small { color: var(--muted); }
|
||||
label.check { display: flex; align-items: center; gap: 8px; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||||
|
||||
/* tables */
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
||||
th { color: var(--muted); font-weight: 550; font-size: 12.5px; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
td.actions { text-align: right; white-space: nowrap; }
|
||||
tr.clickable { cursor: pointer; }
|
||||
tr.clickable:hover td { background: var(--surface-2); }
|
||||
.empty { text-align: center; color: var(--muted); padding: 28px 10px; }
|
||||
|
||||
.badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 12px; background: var(--surface-2); color: var(--muted); }
|
||||
.badge.ok { background: var(--accent-soft); color: var(--accent); }
|
||||
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
||||
|
||||
.banner { border-radius: var(--radius); padding: 12px 16px; margin-bottom: 16px; display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
||||
.banner.warn { background: var(--warn-soft); color: var(--text); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); }
|
||||
.banner.info { background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||
|
||||
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 15px; }
|
||||
.crumbs button { border: none; background: none; color: var(--accent); cursor: pointer; font: inherit; padding: 2px 4px; border-radius: 4px; }
|
||||
.crumbs button:hover { background: var(--surface-2); }
|
||||
.crumbs .sep { color: var(--muted); }
|
||||
|
||||
.dropzone { position: relative; }
|
||||
.dropzone.over::after {
|
||||
content: "Drop files to upload"; position: absolute; inset: 0; display: grid; place-items: center;
|
||||
background: color-mix(in srgb, var(--accent-soft) 85%, transparent); border: 2px dashed var(--accent);
|
||||
border-radius: var(--radius); font-weight: 600; color: var(--accent);
|
||||
}
|
||||
|
||||
.uploads { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.upload { display: grid; grid-template-columns: 1fr 160px 70px; gap: 10px; align-items: center; font-size: 13px; }
|
||||
.bar { height: 6px; background: var(--surface-2); border-radius: 3px; overflow: hidden; }
|
||||
.bar > i { display: block; height: 100%; background: var(--accent); width: 0; transition: width .2s; }
|
||||
|
||||
.logs {
|
||||
font-family: var(--mono); font-size: 12px; background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 12px; height: calc(100vh - 170px); overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
|
||||
}
|
||||
.logs .err { color: var(--danger); }
|
||||
.logs .sys { color: var(--muted); font-style: italic; }
|
||||
|
||||
.secret { display: flex; gap: 6px; align-items: center; }
|
||||
.secret code { background: var(--surface-2); padding: 3px 8px; border-radius: 6px; overflow-wrap: anywhere; }
|
||||
pre.snippet { background: var(--surface-2); padding: 10px 12px; border-radius: 8px; overflow: auto; margin: 0; font-family: var(--mono); font-size: 12px; }
|
||||
|
||||
.phrase { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; counter-reset: w; }
|
||||
.phrase span { background: var(--surface-2); padding: 6px 8px; border-radius: 6px; font-family: var(--mono); counter-increment: w; }
|
||||
.phrase span::before { content: counter(w) ". "; color: var(--muted); }
|
||||
|
||||
.steps { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.steps span { padding: 3px 10px; border-radius: 999px; background: var(--surface-2); color: var(--muted); font-size: 12.5px; }
|
||||
.steps span.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
||||
|
||||
/* modal + toasts */
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgb(0 0 0 / .45); display: grid; place-items: center; padding: 16px; z-index: 10; }
|
||||
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px; width: min(560px, 100%); max-height: 90vh; overflow: auto; }
|
||||
.modal h2 { margin-bottom: 14px; }
|
||||
.modal .foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
||||
|
||||
.toasts { position: fixed; right: 16px; bottom: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 20; }
|
||||
.toast { background: var(--text); color: var(--bg); padding: 10px 14px; border-radius: 8px; max-width: 380px; box-shadow: 0 6px 20px rgb(0 0 0 / .2); }
|
||||
.toast.error { background: var(--danger); color: #fff; }
|
||||
|
||||
.signin { display: grid; place-items: center; min-height: 100%; padding: 16px; }
|
||||
.signin-card { width: min(340px, 100%); display: flex; flex-direction: column; gap: 12px; align-items: stretch; text-align: center; }
|
||||
.signin-card img { align-self: center; }
|
||||
|
||||
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .8s linear infinite; display: inline-block; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app { grid-template-columns: 1fr; }
|
||||
.sidebar { position: static; height: auto; flex-direction: row; flex-wrap: wrap; align-items: center; padding: 10px 16px; }
|
||||
nav { flex-direction: row; flex-wrap: wrap; }
|
||||
.daemon-pill { margin-top: 0; margin-left: auto; }
|
||||
main { padding: 20px 16px 40px; }
|
||||
.upload { grid-template-columns: 1fr 80px 50px; }
|
||||
.hide-sm { display: none; }
|
||||
}
|
||||
916
bundled-addons/pithos/ui/app.js
Normal file
916
bundled-addons/pithos/ui/app.js
Normal file
|
|
@ -0,0 +1,916 @@
|
|||
// Pithos UI. Plain DOM, no build step: every host serves these files as-is.
|
||||
// All user content (object keys, user names, log lines) goes in via
|
||||
// textContent through h(); nothing here assigns innerHTML.
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
function h(tag, attrs = {}, ...children) {
|
||||
const el = document.createElement(tag);
|
||||
for (const [k, v] of Object.entries(attrs || {})) {
|
||||
if (v == null || v === false) continue;
|
||||
if (k === 'class') el.className = v;
|
||||
else if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
|
||||
else if (k === 'dataset') Object.assign(el.dataset, v);
|
||||
else if (v === true) el.setAttribute(k, '');
|
||||
else el.setAttribute(k, v);
|
||||
}
|
||||
for (const c of children.flat(Infinity)) {
|
||||
if (c == null || c === false) continue;
|
||||
el.append(c instanceof Node ? c : String(c));
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
|
||||
// replaceChildren, but skipping the null/false that conditional children produce.
|
||||
function put(el, ...children) {
|
||||
el.replaceChildren(...children.flat(Infinity).filter((c) => c != null && c !== false));
|
||||
}
|
||||
|
||||
async function api(method, path, body, { raw } = {}) {
|
||||
const opts = { method, headers: { 'x-pithos': '1' }, credentials: 'same-origin' };
|
||||
if (body !== undefined) {
|
||||
opts.headers['content-type'] = 'application/json';
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(path, opts);
|
||||
if (res.status === 401 && path !== '/api/session') { showSignin(); throw new Error('signed out'); }
|
||||
if (raw) return res;
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
function toast(msg, kind = '') {
|
||||
const el = h('div', { class: `toast ${kind}` }, msg);
|
||||
$('#toasts').append(el);
|
||||
setTimeout(() => el.remove(), kind === 'error' ? 7000 : 3500);
|
||||
}
|
||||
|
||||
const fail = (e) => { if (e.message !== 'signed out') toast(e.message, 'error'); };
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (n == null || isNaN(n)) return '–';
|
||||
const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
let i = 0;
|
||||
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
||||
return `${i ? n.toFixed(n < 10 ? 2 : 1) : n} ${u[i]}`;
|
||||
}
|
||||
function fmtDate(s) { if (!s) return '–'; const d = new Date(s); return isNaN(d) ? s : d.toLocaleString(); }
|
||||
function fmtDuration(ms) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
if (s < 60) return `${s}s`;
|
||||
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
||||
if (s < 86400) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
||||
return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h`;
|
||||
}
|
||||
|
||||
async function copy(text, what = 'Copied') {
|
||||
try { await navigator.clipboard.writeText(text); toast(what); }
|
||||
catch { prompt('Copy:', text); }
|
||||
}
|
||||
|
||||
function modal(title, body, buttons = []) {
|
||||
return new Promise((resolve) => {
|
||||
const close = (v) => { root.remove(); document.removeEventListener('keydown', onKey); resolve(v); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') close(undefined); };
|
||||
const foot = h('div', { class: 'foot' }, buttons.map((b) => h('button', {
|
||||
class: `btn ${b.kind || ''}`, type: b.submit ? 'submit' : 'button',
|
||||
onclick: b.submit ? null : async () => { const v = b.value ? await b.value() : b.result; if (v !== false) close(v); },
|
||||
}, b.label)));
|
||||
const form = h('form', { class: 'modal', onsubmit: async (e) => {
|
||||
e.preventDefault();
|
||||
const b = buttons.find((x) => x.submit);
|
||||
const v = b.value ? await b.value() : b.result;
|
||||
if (v !== false) close(v);
|
||||
} }, h('h2', {}, title), body, foot);
|
||||
const root = h('div', { class: 'modal-backdrop', onmousedown: (e) => { if (e.target === root) close(undefined); } }, form);
|
||||
$('#modal-root').append(root);
|
||||
document.addEventListener('keydown', onKey);
|
||||
setTimeout(() => (form.querySelector('input, textarea, select') || form.querySelector('.btn.primary'))?.focus(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmModal(title, text, label = 'Delete', kind = 'danger') {
|
||||
return modal(title, h('p', {}, text), [
|
||||
{ label: 'Cancel', result: false },
|
||||
{ label, kind: `primary ${kind === 'danger' ? '' : ''}`, submit: true, result: true },
|
||||
]).then((v) => v === true);
|
||||
}
|
||||
|
||||
function secretRow(label, value) {
|
||||
return h('div', { class: 'stack' },
|
||||
h('span', { class: 'small muted' }, label),
|
||||
h('div', { class: 'secret' }, h('code', {}, value), h('button', { class: 'btn small', type: 'button', onclick: () => copy(value) }, 'Copy')));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- state
|
||||
|
||||
const state = {
|
||||
status: null,
|
||||
login: { state: 'idle' },
|
||||
logs: [],
|
||||
logSeq: 0,
|
||||
selectedUser: localStorage.getItem('pithos.user') || '',
|
||||
};
|
||||
const listeners = new Set();
|
||||
const onState = (fn) => { listeners.add(fn); return () => listeners.delete(fn); };
|
||||
const emit = () => listeners.forEach((fn) => fn());
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
state.status = await api('GET', '/api/status');
|
||||
state.login = state.status.login;
|
||||
renderPill();
|
||||
emit();
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
function connectEvents() {
|
||||
const es = new EventSource('/api/events');
|
||||
es.addEventListener('status', (e) => {
|
||||
if (!state.status) return;
|
||||
state.status.daemon = JSON.parse(e.data);
|
||||
renderPill();
|
||||
emit();
|
||||
});
|
||||
es.addEventListener('log', (e) => {
|
||||
const l = JSON.parse(e.data);
|
||||
if (l.seq <= state.logSeq) return;
|
||||
state.logSeq = l.seq;
|
||||
state.logs.push(l);
|
||||
if (state.logs.length > 2000) state.logs.shift();
|
||||
logSink?.(l);
|
||||
});
|
||||
es.addEventListener('login', (e) => { state.login = JSON.parse(e.data); emit(); });
|
||||
es.addEventListener('install', (e) => { const p = JSON.parse(e.data); if (p.phase === 'extract') toast('Verified checksum, installing…'); });
|
||||
es.onerror = () => { /* EventSource reconnects on its own */ };
|
||||
}
|
||||
|
||||
function daemonState() {
|
||||
const s = state.status;
|
||||
if (!s) return 'stopped';
|
||||
if (s.daemon.state === 'stopped' && s.external) return 'external';
|
||||
if (s.daemon.state === 'crashed' && s.daemon.needsLogin) return 'not connected';
|
||||
return s.daemon.state;
|
||||
}
|
||||
|
||||
function renderPill() {
|
||||
const st = daemonState();
|
||||
const label = { running: 's3d running', starting: 'Starting…', stopping: 'Stopping…', crashed: 's3d stopped (error)', 'not connected': 'Not connected to Sia', stopped: 's3d stopped', external: 's3d running (external)' }[st];
|
||||
const pill = $('#daemon-pill');
|
||||
pill.querySelector('.dot').className = `dot ${st === 'external' ? 'running' : st === 'not connected' ? 'starting' : st}`;
|
||||
pill.querySelector('.label').textContent = label;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- router
|
||||
|
||||
let cleanup = [];
|
||||
let logSink = null;
|
||||
const views = { overview, buckets, users, setup, settings, logs };
|
||||
|
||||
function route() {
|
||||
cleanup.forEach((fn) => fn());
|
||||
cleanup = [];
|
||||
logSink = null;
|
||||
const [, name = 'overview', ...rest] = location.hash.replace(/^#/, '').split('/');
|
||||
const view = views[name] ? name : 'overview';
|
||||
for (const a of document.querySelectorAll('#nav a')) a.classList.toggle('active', a.dataset.view === view);
|
||||
const main = $('#main');
|
||||
put(main);
|
||||
views[view](main, rest.map(decodeURIComponent));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- overview
|
||||
|
||||
function overview(main) {
|
||||
const daemonCard = h('div', { class: 'card' });
|
||||
const statsCard = h('div', { class: 'card' });
|
||||
const banners = h('div');
|
||||
main.append(
|
||||
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Overview'), h('p', { class: 'muted' }, 'Your s3d gateway and its upload pipeline to Sia.'))),
|
||||
banners,
|
||||
h('div', { class: 'grid two' }, daemonCard, statsCard),
|
||||
);
|
||||
|
||||
function draw() {
|
||||
const s = state.status;
|
||||
if (!s) return;
|
||||
put(banners, ...bannersFor(s));
|
||||
const st = daemonState();
|
||||
const busy = st === 'starting' || st === 'stopping';
|
||||
const running = st === 'running' || st === 'starting';
|
||||
put(daemonCard,
|
||||
h('div', { class: 'row spread' }, h('h2', {}, 'Gateway'), h('span', { class: `badge ${st === 'running' || st === 'external' ? 'ok' : st === 'crashed' ? 'warn' : ''}` }, st)),
|
||||
h('dl', { class: 'kv' },
|
||||
h('dt', {}, 'S3 endpoint'), h('dd', {}, h('code', {}, `http://${s.config.apiAddress}`)),
|
||||
s.config.https && [h('dt', {}, 'S3 HTTPS'), h('dd', {}, h('code', {}, `https://${s.config.https}`))],
|
||||
h('dt', {}, 's3d'), h('dd', {}, s.s3d?.version ? `v${s.s3d.version}` : (s.daemon.binary ? 'unknown version' : 'not installed')),
|
||||
h('dt', {}, 'Uptime'), h('dd', {}, st === 'running' && s.daemon.startedAt ? fmtDuration(Date.now() - s.daemon.startedAt) : '–'),
|
||||
h('dt', {}, 'Config'), h('dd', {}, h('code', {}, s.config.file)),
|
||||
h('dt', {}, 'Data'), h('dd', {}, h('code', {}, s.config.directory)),
|
||||
),
|
||||
h('div', { class: 'row', style: 'margin-top:16px' },
|
||||
st === 'external'
|
||||
? h('span', { class: 'muted small' }, 'Another s3d process is serving this config. Stop it to manage it from here.')
|
||||
: [
|
||||
h('button', { class: 'btn primary', disabled: busy || running || !s.daemon.binary, onclick: () => daemonAction('start') }, 'Start'),
|
||||
h('button', { class: 'btn', disabled: busy || !running, onclick: () => daemonAction('restart') }, 'Restart'),
|
||||
h('button', { class: 'btn danger', disabled: busy || !running, onclick: () => daemonAction('stop') }, 'Stop'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function drawStats() {
|
||||
const st = daemonState();
|
||||
if (st !== 'running' && st !== 'external') {
|
||||
put(statsCard, h('h2', {}, 'Upload pipeline'), h('p', { class: 'muted' }, 'Start s3d to see live upload stats.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const x = await api('GET', '/api/stats');
|
||||
const stat = (v, l, bad) => h('div', { class: `stat ${bad ? 'bad' : ''}` }, h('div', { class: 'v' }, v), h('div', { class: 'l' }, l));
|
||||
put(statsCard,
|
||||
h('div', { class: 'row spread' }, h('h2', {}, 'Upload pipeline'),
|
||||
h('button', { class: 'btn small', disabled: !x.pendingObjects, title: 'Upload everything waiting in the local buffer now, even if slabs are not full', onclick: flushNow }, 'Flush now')),
|
||||
h('div', { class: 'stats' },
|
||||
stat(x.pendingObjects, `pending · ${fmtBytes(x.pendingSize)}`),
|
||||
stat(x.uploadedObjects, `on Sia · ${fmtBytes(x.uploadedSize)}`),
|
||||
stat(x.unpinnedObjects, 'awaiting pin'),
|
||||
stat(x.multipartUploads, 'open multipart'),
|
||||
stat(x.failedUploads, 'failed uploads', x.failedUploads > 0),
|
||||
stat(x.orphanedObjects, 'orphaned', x.orphanedObjects > 0),
|
||||
),
|
||||
h('p', { class: 'small muted', style: 'margin:12px 0 0' }, 'New objects wait in a local buffer and are packed into full slabs before upload, which keeps wasted space down.'),
|
||||
);
|
||||
} catch (e) {
|
||||
put(statsCard, h('h2', {}, 'Upload pipeline'), h('p', { class: 'error' }, e.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function flushNow(ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
put(btn, h('span', { class: 'spinner' }), ' Flushing…');
|
||||
try { await api('POST', '/api/flush'); toast('All pending objects are on Sia'); }
|
||||
catch (e) { fail(e); }
|
||||
drawStats();
|
||||
}
|
||||
|
||||
draw();
|
||||
drawStats();
|
||||
cleanup.push(onState(draw));
|
||||
let lastState = daemonState();
|
||||
cleanup.push(onState(() => { if (daemonState() !== lastState) { lastState = daemonState(); drawStats(); } }));
|
||||
const t = setInterval(() => { draw(); drawStats(); }, 4000);
|
||||
cleanup.push(() => clearInterval(t));
|
||||
}
|
||||
|
||||
function bannersFor(s) {
|
||||
const out = [];
|
||||
if (!s.daemon.binary) {
|
||||
out.push(h('div', { class: 'banner warn' },
|
||||
h('span', {}, `s3d is not installed. Pithos can download the official v${s.s3d.pinned} release and check its SHA-256.`),
|
||||
s.s3d.installable && h('button', { class: 'btn primary', onclick: installBinary }, `Install s3d v${s.s3d.pinned}`)));
|
||||
} else if (s.s3d?.outdated) {
|
||||
out.push(h('div', { class: 'banner warn' },
|
||||
h('span', {}, `s3d v${s.s3d.version} is older than v${s.s3d.pinned}. User and key listing needs v0.2.0 or newer.`),
|
||||
s.s3d.installable && h('button', { class: 'btn', onclick: installBinary }, `Install v${s.s3d.pinned}`)));
|
||||
}
|
||||
if (s.registration?.registered === false || s.daemon.needsLogin) {
|
||||
out.push(h('div', { class: 'banner info' },
|
||||
h('span', {}, 'This s3d is not connected to a Sia indexer yet.'),
|
||||
h('a', { class: 'btn primary', href: '#/setup' }, 'Connect to Sia')));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function installBinary(ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
put(btn, h('span', { class: 'spinner' }), ' Downloading…');
|
||||
try {
|
||||
const r = await api('POST', '/api/binary/install');
|
||||
toast(`Installed s3d v${r.version?.version || ''}`);
|
||||
} catch (e) { fail(e); }
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
async function daemonAction(action) {
|
||||
try {
|
||||
await api('POST', `/api/daemon/${action}`);
|
||||
await refreshStatus();
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- setup (indexer login)
|
||||
|
||||
function setup(main) {
|
||||
const body = h('div', { class: 'card' });
|
||||
main.append(
|
||||
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Sia connection'),
|
||||
h('p', { class: 'muted' }, 'Register this s3d with a Sia indexer. You do this once; the indexer then handles hosts and contracts for it.'))),
|
||||
body,
|
||||
);
|
||||
|
||||
let draft = { indexerUrl: 'https://sia.storage', phrase: '', mode: 'existing' };
|
||||
|
||||
function draw() {
|
||||
let l = state.login;
|
||||
// Registered already (e.g. on an earlier run): show that instead of the form.
|
||||
if (l.state === 'idle' && state.status?.registration?.registered) l = { state: 'already', indexerUrl: state.status.registration.indexerUrl };
|
||||
const step = ['idle', 'failed'].includes(l.state) ? 0 : ['done', 'already'].includes(l.state) ? 2 : 1;
|
||||
const steps = h('div', { class: 'steps' }, ['Recovery phrase', 'Approve on the indexer', 'Connected'].map((t, i) => h('span', { class: i === step ? 'on' : '' }, `${i + 1}. ${t}`)));
|
||||
|
||||
if (l.state === 'done' || l.state === 'already') {
|
||||
put(body, steps,
|
||||
h('h2', {}, l.state === 'done' ? 'Connected' : 'Already connected'),
|
||||
h('p', {}, l.indexerUrl ? ['This s3d is registered with ', h('code', {}, l.indexerUrl), '.'] : 'This s3d is registered with an indexer.'),
|
||||
h('div', { class: 'row' },
|
||||
daemonState() !== 'running' && h('button', { class: 'btn primary', onclick: async () => { await daemonAction(daemonState() === 'crashed' ? 'restart' : 'start'); location.hash = '#/overview'; } }, 'Start s3d'),
|
||||
h('a', { class: 'btn', href: '#/users' }, 'Create a user'),
|
||||
h('button', { class: 'btn', onclick: () => api('POST', '/api/login/cancel').catch(fail) }, 'Done')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (l.state === 'confirm') {
|
||||
const words = (l.generatedPhrase || '').split(/\s+/);
|
||||
const ack = h('input', { type: 'checkbox', id: 'ack' });
|
||||
const go = h('button', { class: 'btn primary', disabled: true, onclick: () => api('POST', '/api/login/confirm').catch(fail) }, 'Continue');
|
||||
ack.addEventListener('change', () => { go.disabled = !ack.checked; });
|
||||
put(body, steps,
|
||||
h('h2', {}, 'Write down your new recovery phrase'),
|
||||
h('p', {}, 'This phrase is the only way to register this storage again on another machine or after a reinstall. Pithos does not keep it. Store it offline.'),
|
||||
h('div', { class: 'phrase' }, words.map((w) => h('span', {}, w))),
|
||||
h('div', { class: 'row', style: 'margin-top:14px' }, h('button', { class: 'btn small', onclick: () => copy(l.generatedPhrase, 'Phrase copied - clear your clipboard after storing it') }, 'Copy')),
|
||||
h('label', { class: 'check', style: 'margin:16px 0' }, ack, 'I have stored the phrase somewhere safe'),
|
||||
h('div', { class: 'row' }, go, h('button', { class: 'btn', onclick: cancel }, 'Cancel')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (l.state === 'terminal') {
|
||||
put(body, steps,
|
||||
h('h2', {}, 'Finish in the console window'),
|
||||
h('p', {}, 'A console window running "s3d login" has opened. Enter the indexer URL and your recovery phrase there, then open the approval link it prints. This page updates when the window closes.'),
|
||||
h('p', { class: 'muted row' }, h('span', { class: 'spinner' }), ' Waiting for the login window…'),
|
||||
h('div', { class: 'row' }, h('button', { class: 'btn danger', onclick: cancel }, 'Cancel')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (step === 1) {
|
||||
const waiting = l.state === 'approve' ? null : h('p', { class: 'muted row' }, h('span', { class: 'spinner' }), ' Talking to the indexer…');
|
||||
put(body, steps,
|
||||
h('h2', {}, 'Approve the connection'),
|
||||
waiting,
|
||||
l.approvalUrl && [
|
||||
h('p', {}, 'Open this link, sign in to your indexer account and approve the "S3d" app. This page continues by itself once you approve.'),
|
||||
h('div', { class: 'secret' }, h('code', {}, l.approvalUrl)),
|
||||
h('div', { class: 'row', style: 'margin-top:12px' },
|
||||
h('button', { class: 'btn primary', onclick: () => openLink(l.approvalUrl) }, 'Open approval page'),
|
||||
h('button', { class: 'btn', onclick: () => copy(l.approvalUrl) }, 'Copy link')),
|
||||
h('p', { class: 'muted row', style: 'margin-top:16px' }, h('span', { class: 'spinner' }), ' Waiting for approval…'),
|
||||
],
|
||||
h('div', { class: 'row', style: 'margin-top:12px' }, h('button', { class: 'btn danger', onclick: cancel }, 'Cancel')));
|
||||
return;
|
||||
}
|
||||
|
||||
// step 0: form
|
||||
const url = h('input', { type: 'url', value: draft.indexerUrl, required: true, oninput: (e) => { draft.indexerUrl = e.target.value; } });
|
||||
const phrase = h('textarea', { rows: 3, autocomplete: 'off', spellcheck: 'false', placeholder: 'twelve words separated by spaces', oninput: (e) => { draft.phrase = e.target.value; } });
|
||||
phrase.value = draft.phrase;
|
||||
const radios = h('div', { class: 'stack' },
|
||||
h('label', { class: 'check' }, h('input', { type: 'radio', name: 'mode', checked: draft.mode === 'existing', onchange: () => { draft.mode = 'existing'; draw(); } }), 'I have a recovery phrase'),
|
||||
h('label', { class: 'check' }, h('input', { type: 'radio', name: 'mode', checked: draft.mode === 'new', onchange: () => { draft.mode = 'new'; draw(); } }), 'Generate a new one'));
|
||||
put(body, steps,
|
||||
l.state === 'failed' && h('div', { class: 'banner warn' }, h('span', {}, l.error || 'Login failed')),
|
||||
h('form', { class: 'stack', onsubmit: start },
|
||||
h('label', { class: 'field' }, h('span', {}, 'Indexer'), url, h('small', {}, 'https://sia.storage is the hosted indexer run by the Sia Foundation. Use your own indexd URL if you run one.')),
|
||||
radios,
|
||||
draft.mode === 'existing' && h('label', { class: 'field' }, h('span', {}, 'Recovery phrase'), phrase,
|
||||
h('small', {}, 'Sent only to the local s3d process. It derives this app\'s key and is not stored by Pithos.')),
|
||||
h('div', { class: 'row' }, h('button', { class: 'btn primary', type: 'submit' }, 'Connect'))));
|
||||
}
|
||||
|
||||
async function start(e) {
|
||||
e.preventDefault();
|
||||
const phrase = draft.mode === 'existing' ? draft.phrase.trim() : '';
|
||||
if (draft.mode === 'existing' && phrase.split(/\s+/).length !== 12) return toast('A recovery phrase is 12 words', 'error');
|
||||
try {
|
||||
await api('POST', '/api/login', { indexerUrl: draft.indexerUrl, phrase });
|
||||
draft.phrase = '';
|
||||
} catch (err) { fail(err); }
|
||||
}
|
||||
|
||||
async function cancel() { try { await api('POST', '/api/login/cancel'); } catch (e) { fail(e); } }
|
||||
|
||||
draw();
|
||||
cleanup.push(onState(draw));
|
||||
}
|
||||
|
||||
async function openLink(url) {
|
||||
const host = state.status?.host;
|
||||
if (host === 'desktop' || host === 'theseus') {
|
||||
try { await api('POST', '/api/open-external', { url }); return; } catch { /* fall through */ }
|
||||
}
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- users & keys
|
||||
|
||||
function users(main) {
|
||||
const list = h('div', { class: 'card' });
|
||||
main.append(
|
||||
h('div', { class: 'page-head' },
|
||||
h('div', {}, h('h1', {}, 'Users & keys'), h('p', { class: 'muted' }, 'Each S3 user owns its own buckets. Access keys let S3 clients (aws, rclone, apps) act as that user.')),
|
||||
h('button', { class: 'btn primary', onclick: createUser }, 'New user')),
|
||||
list,
|
||||
);
|
||||
|
||||
let expanded = new Set();
|
||||
|
||||
async function load() {
|
||||
let rows;
|
||||
try { rows = await api('GET', '/api/users'); }
|
||||
catch (e) { put(list, h('p', { class: 'error' }, e.message)); return; }
|
||||
if (!rows.length) {
|
||||
put(list, h('div', { class: 'empty' }, h('p', {}, 'No users yet.'), h('button', { class: 'btn primary', onclick: createUser }, 'Create the first user')));
|
||||
return;
|
||||
}
|
||||
const keys = await api('GET', '/api/keys').catch(() => []);
|
||||
put(list, h('table', {},
|
||||
h('thead', {}, h('tr', {}, h('th', {}, 'User'), h('th', {}, 'Access keys'), h('th', {}))),
|
||||
h('tbody', {}, rows.flatMap((u) => {
|
||||
const open = expanded.has(u.name);
|
||||
const userKeys = keys.filter((k) => k.user === u.name);
|
||||
const main = h('tr', { class: 'clickable', onclick: () => { open ? expanded.delete(u.name) : expanded.add(u.name); load(); } },
|
||||
h('td', {}, h('strong', {}, u.name)),
|
||||
h('td', {}, `${u.keys.length}`),
|
||||
h('td', { class: 'actions' },
|
||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); state.selectedUser = u.name; localStorage.setItem('pithos.user', u.name); location.hash = '#/buckets'; } }, 'Buckets'),
|
||||
' ',
|
||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
|
||||
' ',
|
||||
h('button', { class: 'btn small danger', onclick: (e) => { e.stopPropagation(); deleteUser(u.name); } }, 'Delete')));
|
||||
if (!open) return [main];
|
||||
const detail = h('tr', {}, h('td', { colspan: 3, style: 'background:var(--surface-2)' },
|
||||
userKeys.length ? h('table', {}, h('tbody', {}, userKeys.map((k) => h('tr', {},
|
||||
h('td', {}, h('code', {}, k.accessKeyId)),
|
||||
h('td', { class: 'actions' },
|
||||
h('button', { class: 'btn small', onclick: () => showKey(k) }, 'Show secret & config'),
|
||||
' ',
|
||||
h('button', { class: 'btn small danger', onclick: () => deleteKey(k.accessKeyId) }, 'Revoke'))))))
|
||||
: h('p', { class: 'muted', style: 'margin:0' }, 'No keys. Create one to use S3 clients or browse buckets.')));
|
||||
return [main, detail];
|
||||
}))));
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const name = h('input', { type: 'text', required: true, pattern: '[A-Za-z0-9][A-Za-z0-9._@-]{0,63}', placeholder: 'e.g. backups' });
|
||||
const withKey = h('input', { type: 'checkbox', checked: true });
|
||||
const v = await modal('New user', h('div', { class: 'stack' },
|
||||
h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, 'Letters, digits, . _ @ -')),
|
||||
h('label', { class: 'check' }, withKey, 'Create an access key now')),
|
||||
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => ({ name: name.value.trim(), withKey: withKey.checked }) }]);
|
||||
if (!v) return;
|
||||
try {
|
||||
const r = await api('POST', '/api/users', v);
|
||||
toast(`Created ${r.name}`);
|
||||
expanded.add(r.name);
|
||||
if (!state.selectedUser) { state.selectedUser = r.name; localStorage.setItem('pithos.user', r.name); }
|
||||
await load();
|
||||
if (r.key) showKey(r.key, true);
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function newKey(user) {
|
||||
try {
|
||||
const k = await api('POST', '/api/keys', { user });
|
||||
expanded.add(user);
|
||||
await load();
|
||||
showKey(k, true);
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function deleteUser(name) {
|
||||
if (!(await confirmModal(`Delete ${name}?`, 'Its access keys stop working immediately. s3d refuses if the user still owns buckets.'))) return;
|
||||
try { await api('DELETE', `/api/users/${encodeURIComponent(name)}`); toast(`Deleted ${name}`); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function deleteKey(id) {
|
||||
if (!(await confirmModal('Revoke this key?', `Clients using ${id} will be refused from now on.`, 'Revoke'))) return;
|
||||
try { await api('DELETE', `/api/keys/${encodeURIComponent(id)}`); toast('Key revoked'); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
function showKey(k, fresh) {
|
||||
const s = state.status;
|
||||
const endpoint = `http://${s?.config.apiAddress || '127.0.0.1:8000'}`;
|
||||
const aws = `aws configure set aws_access_key_id ${k.accessKeyId} --profile ${k.user}
|
||||
aws configure set aws_secret_access_key ${k.secretKey} --profile ${k.user}
|
||||
aws configure set region us-east-1 --profile ${k.user}
|
||||
aws --profile ${k.user} --endpoint-url ${endpoint} s3 ls`;
|
||||
const rclone = `[sia-${k.user}]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = ${k.accessKeyId}
|
||||
secret_access_key = ${k.secretKey}
|
||||
endpoint = ${endpoint}
|
||||
region = us-east-1
|
||||
force_path_style = true`;
|
||||
const env = `AWS_ACCESS_KEY_ID=${k.accessKeyId}
|
||||
AWS_SECRET_ACCESS_KEY=${k.secretKey}
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ENDPOINT_URL=${endpoint}`;
|
||||
const tabs = { 'aws cli': aws, rclone, env };
|
||||
const pre = h('pre', { class: 'snippet' }, aws);
|
||||
const tabBtns = h('div', { class: 'row' }, Object.keys(tabs).map((t, i) => h('button', {
|
||||
type: 'button', class: `btn small ${i === 0 ? 'primary' : ''}`,
|
||||
onclick: (e) => { for (const b of tabBtns.children) b.classList.remove('primary'); e.currentTarget.classList.add('primary'); pre.textContent = tabs[t]; },
|
||||
}, t)));
|
||||
modal(fresh ? `Access key for ${k.user}` : `Key ${k.accessKeyId}`, h('div', { class: 'stack' },
|
||||
fresh && h('p', { class: 'small muted', style: 'margin:0' }, 'Treat the secret like a password. You can view it again here later.'),
|
||||
secretRow('Access key ID', k.accessKeyId),
|
||||
secretRow('Secret key', k.secretKey),
|
||||
h('h3', { style: 'margin-top:8px' }, 'Client config'),
|
||||
tabBtns, pre,
|
||||
h('div', {}, h('button', { class: 'btn small', type: 'button', onclick: () => copy(pre.textContent) }, 'Copy snippet'))),
|
||||
[{ label: 'Done', kind: 'primary', submit: true, result: true }]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- buckets & objects
|
||||
|
||||
function buckets(main, [bucket, ...prefixParts]) {
|
||||
const head = h('div', { class: 'page-head' });
|
||||
const body = h('div');
|
||||
main.append(head, body);
|
||||
|
||||
let users = [];
|
||||
const userSel = h('select', { onchange: (e) => { state.selectedUser = e.target.value; localStorage.setItem('pithos.user', e.target.value); location.hash = '#/buckets'; route(); } });
|
||||
|
||||
(async () => {
|
||||
try { users = await api('GET', '/api/users'); }
|
||||
catch (e) { put(body, h('div', { class: 'card' }, h('p', { class: 'error' }, e.message))); return; }
|
||||
const usable = users.filter((u) => u.keys.length);
|
||||
if (!usable.length) {
|
||||
put(head, h('div', {}, h('h1', {}, 'Buckets')));
|
||||
put(body, h('div', { class: 'card empty' }, h('p', {}, users.length ? 'None of your users has an access key yet.' : 'Create a user first; buckets belong to users.'), h('a', { class: 'btn primary', href: '#/users' }, 'Users & keys')));
|
||||
return;
|
||||
}
|
||||
if (!usable.some((u) => u.name === state.selectedUser)) state.selectedUser = usable[0].name;
|
||||
put(userSel, ...usable.map((u) => h('option', { value: u.name, selected: u.name === state.selectedUser }, u.name)));
|
||||
const st = daemonState();
|
||||
if (st !== 'running' && st !== 'external') {
|
||||
put(head, h('div', {}, h('h1', {}, 'Buckets')));
|
||||
put(body, h('div', { class: 'card empty' }, h('p', {}, 's3d is not running.'), h('button', { class: 'btn primary', onclick: async () => { await daemonAction('start'); setTimeout(route, 1500); } }, 'Start s3d')));
|
||||
return;
|
||||
}
|
||||
if (bucket) objectBrowser(head, body, state.selectedUser, bucket, prefixParts.join('/'), userSel);
|
||||
else bucketList(head, body, state.selectedUser, userSel);
|
||||
})();
|
||||
}
|
||||
|
||||
function userPicker(userSel) {
|
||||
return h('label', { class: 'row small' }, h('span', { class: 'muted' }, 'Acting as'), h('span', { style: 'width:180px' }, userSel));
|
||||
}
|
||||
|
||||
async function bucketList(head, body, user, userSel) {
|
||||
put(head,
|
||||
h('div', {}, h('h1', {}, 'Buckets'), h('p', { class: 'muted' }, 'Buckets owned by the selected user.')),
|
||||
h('div', { class: 'row' }, userPicker(userSel), h('button', { class: 'btn primary', onclick: create }, 'New bucket')));
|
||||
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
||||
put(body, card);
|
||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets`;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const list = await api('GET', base);
|
||||
if (!list.length) { put(card, h('div', { class: 'empty' }, h('p', {}, 'No buckets yet.'), h('button', { class: 'btn primary', onclick: create }, 'Create a bucket'))); return; }
|
||||
put(card, h('table', {},
|
||||
h('thead', {}, h('tr', {}, h('th', {}, 'Name'), h('th', { class: 'hide-sm' }, 'Created'), h('th', {}, 'Access'), h('th', {}))),
|
||||
h('tbody', {}, list.map((b) => {
|
||||
const access = h('td', {}, h('span', { class: 'badge' }, '…'));
|
||||
api('GET', `${base}/${encodeURIComponent(b.name)}/policy`).then((p) => {
|
||||
put(access, h('span', { class: `badge ${p.public === 'private' ? '' : 'warn'}` }, { private: 'private', read: 'public read', 'read-list': 'public read + list', custom: 'custom policy' }[p.public]));
|
||||
}).catch(() => put(access, h('span', { class: 'badge' }, '?')));
|
||||
return h('tr', { class: 'clickable', onclick: () => { location.hash = `#/buckets/${encodeURIComponent(b.name)}`; } },
|
||||
h('td', {}, h('strong', {}, b.name)),
|
||||
h('td', { class: 'hide-sm muted' }, fmtDate(b.created)),
|
||||
access,
|
||||
h('td', { class: 'actions' },
|
||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); accessDialog(user, b.name).then(load); } }, 'Access'),
|
||||
' ',
|
||||
h('button', { class: 'btn small danger', onclick: (e) => { e.stopPropagation(); remove(b.name); } }, 'Delete')));
|
||||
}))));
|
||||
} catch (e) { put(card, h('p', { class: 'error' }, e.message)); }
|
||||
}
|
||||
|
||||
async function create() {
|
||||
const name = h('input', { type: 'text', required: true, pattern: '[a-z0-9][a-z0-9.\\-]{1,61}[a-z0-9]', placeholder: 'my-bucket' });
|
||||
const v = await modal('New bucket', h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, '3-63 characters: lowercase letters, digits, dots and hyphens. Names are unique across this s3d.')),
|
||||
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => name.value.trim() }]);
|
||||
if (!v) return;
|
||||
try { await api('POST', base, { name: v }); toast(`Created ${v}`); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function remove(name) {
|
||||
if (!(await confirmModal(`Delete bucket ${name}?`, 'The bucket must be empty. This cannot be undone.'))) return;
|
||||
try { await api('DELETE', `${base}/${encodeURIComponent(name)}`); toast(`Deleted ${name}`); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
async function accessDialog(user, bucket) {
|
||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets/${encodeURIComponent(bucket)}/policy`;
|
||||
let current = 'private';
|
||||
try { current = (await api('GET', base)).public; } catch (e) { return fail(e); }
|
||||
const opts = [
|
||||
['private', 'Private', 'Only this user\'s access keys can read.'],
|
||||
['read', 'Public read', 'Anyone who knows an object\'s URL can download it.'],
|
||||
['read-list', 'Public read + list', 'Anyone can also list the bucket\'s contents.'],
|
||||
];
|
||||
let choice = current === 'custom' ? 'private' : current;
|
||||
const endpoint = `http://${state.status?.config.apiAddress}`;
|
||||
const v = await modal(`Access for ${bucket}`, h('div', { class: 'stack' },
|
||||
current === 'custom' && h('div', { class: 'banner warn' }, 'This bucket has a custom policy. Saving replaces it.'),
|
||||
opts.map(([val, label, desc]) => h('label', { class: 'check', style: 'align-items:flex-start' },
|
||||
h('input', { type: 'radio', name: 'acc', checked: choice === val, onchange: () => { choice = val; } }),
|
||||
h('span', {}, h('strong', {}, label), h('br'), h('span', { class: 'small muted' }, desc)))),
|
||||
h('p', { class: 'small muted', style: 'margin:0' }, 'Public objects are served at ', h('code', {}, `${endpoint}/${bucket}/<key>`), '. Writes and deletes always need a key.')),
|
||||
[{ label: 'Cancel', result: null }, { label: 'Save', kind: 'primary', submit: true, value: () => choice }]);
|
||||
if (!v || v === current) return;
|
||||
try { await api('PUT', base, { mode: v }); toast('Access updated'); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
||||
if (prefix && !prefix.endsWith('/')) prefix += '/';
|
||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets/${encodeURIComponent(bucket)}`;
|
||||
const go = (p) => { location.hash = `#/buckets/${encodeURIComponent(bucket)}${p ? '/' + p.split('/').filter(Boolean).map(encodeURIComponent).join('/') : ''}`; };
|
||||
|
||||
const crumbs = h('div', { class: 'crumbs' },
|
||||
h('button', { onclick: () => { location.hash = '#/buckets'; } }, 'Buckets'), h('span', { class: 'sep' }, '/'),
|
||||
h('button', { onclick: () => go('') }, bucket),
|
||||
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
|
||||
|
||||
const fileInput = h('input', { type: 'file', multiple: true, hidden: true, onchange: (e) => { uploadFiles([...e.target.files]); e.target.value = ''; } });
|
||||
put(head,
|
||||
h('div', { class: 'stack' }, crumbs),
|
||||
h('div', { class: 'row' }, userPicker(userSel),
|
||||
h('button', { class: 'btn', onclick: newFolder }, 'New folder'),
|
||||
h('button', { class: 'btn', onclick: () => accessDialog(user, bucket) }, 'Access'),
|
||||
h('button', { class: 'btn primary', onclick: () => fileInput.click() }, 'Upload'), fileInput));
|
||||
|
||||
const uploads = h('div', { class: 'uploads' });
|
||||
const card = h('div', { class: 'card dropzone' }, h('p', { class: 'muted' }, 'Loading…'));
|
||||
put(body, uploads, card);
|
||||
|
||||
let depth = 0;
|
||||
card.addEventListener('dragenter', (e) => { e.preventDefault(); depth++; card.classList.add('over'); });
|
||||
card.addEventListener('dragleave', () => { if (--depth <= 0) { depth = 0; card.classList.remove('over'); } });
|
||||
card.addEventListener('dragover', (e) => e.preventDefault());
|
||||
card.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); depth = 0; card.classList.remove('over');
|
||||
uploadFiles([...e.dataTransfer.files]);
|
||||
});
|
||||
|
||||
let pages = [];
|
||||
async function load(token) {
|
||||
try {
|
||||
const q = new URLSearchParams({ prefix });
|
||||
if (token) q.set('token', token);
|
||||
const page = await api('GET', `${base}/objects?${q}`);
|
||||
pages = token ? [...pages, page] : [page];
|
||||
draw();
|
||||
} catch (e) { put(card, h('p', { class: 'error' }, e.message)); }
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const folders = pages.flatMap((p) => p.folders);
|
||||
const objects = pages.flatMap((p) => p.objects).filter((o) => o.key !== prefix);
|
||||
const last = pages[pages.length - 1];
|
||||
if (!folders.length && !objects.length) {
|
||||
put(card, h('div', { class: 'empty' }, h('p', {}, prefix ? 'This folder is empty.' : 'This bucket is empty.'), h('p', { class: 'small' }, 'Drag files here or use Upload.')));
|
||||
return;
|
||||
}
|
||||
put(card, h('table', {},
|
||||
h('thead', {}, h('tr', {}, h('th', {}, 'Name'), h('th', { class: 'num' }, 'Size'), h('th', { class: 'hide-sm' }, 'Modified'), h('th', {}))),
|
||||
h('tbody', {},
|
||||
folders.map((f) => h('tr', { class: 'clickable', onclick: () => go(f) },
|
||||
h('td', {}, '📁 ', f.slice(prefix.length)),
|
||||
h('td', { class: 'num muted' }, '–'), h('td', { class: 'hide-sm' }),
|
||||
h('td', { class: 'actions' }, h('button', { class: 'btn small danger', onclick: (e) => { e.stopPropagation(); deleteFolder(f); } }, 'Delete')))),
|
||||
objects.map((o) => h('tr', {},
|
||||
h('td', {}, h('a', { href: `${base}/object?key=${encodeURIComponent(o.key)}`, download: '' }, o.key.slice(prefix.length))),
|
||||
h('td', { class: 'num' }, fmtBytes(o.size)),
|
||||
h('td', { class: 'hide-sm muted' }, fmtDate(o.modified)),
|
||||
h('td', { class: 'actions' },
|
||||
h('button', { class: 'btn small', onclick: () => share(o.key) }, 'Share'),
|
||||
' ',
|
||||
h('button', { class: 'btn small danger', onclick: () => deleteObject(o.key) }, 'Delete')))))),
|
||||
last?.truncated && h('div', { class: 'row', style: 'justify-content:center;margin-top:12px' }, h('button', { class: 'btn', onclick: () => load(last.nextToken) }, 'Load more')));
|
||||
}
|
||||
|
||||
function uploadFiles(files) {
|
||||
for (const file of files) {
|
||||
if (file.size > 5 * 1024 ** 3) { toast(`${file.name}: single uploads are limited to 5 GiB`, 'error'); continue; }
|
||||
const key = prefix + file.name;
|
||||
const bar = h('i');
|
||||
const pct = h('span', { class: 'muted' }, '0%');
|
||||
const row = h('div', { class: 'upload' }, h('span', {}, key), h('div', { class: 'bar' }, bar), pct);
|
||||
uploads.append(row);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', `${base}/object?key=${encodeURIComponent(key)}`);
|
||||
xhr.setRequestHeader('x-pithos', '1');
|
||||
if (file.type) xhr.setRequestHeader('content-type', file.type);
|
||||
xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const p = Math.round((e.loaded / e.total) * 100); bar.style.width = p + '%'; pct.textContent = p + '%'; } };
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) { pct.textContent = 'done'; setTimeout(() => row.remove(), 1500); load(); }
|
||||
else { pct.textContent = 'failed'; pct.className = 'error'; let m = xhr.statusText; try { m = JSON.parse(xhr.responseText).error; } catch {} toast(`${file.name}: ${m}`, 'error'); }
|
||||
};
|
||||
xhr.onerror = () => { pct.textContent = 'failed'; pct.className = 'error'; toast(`${file.name}: network error`, 'error'); };
|
||||
xhr.send(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function newFolder() {
|
||||
const name = h('input', { type: 'text', required: true, pattern: '[^/]+', placeholder: 'photos' });
|
||||
const v = await modal('New folder', h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, 'S3 has no real folders. This opens an empty prefix; it appears once you upload into it.')),
|
||||
[{ label: 'Cancel', result: null }, { label: 'Open', kind: 'primary', submit: true, value: () => name.value.trim() }]);
|
||||
if (v) go(prefix + v);
|
||||
}
|
||||
|
||||
async function deleteObject(key) {
|
||||
if (!(await confirmModal('Delete object?', key))) return;
|
||||
try { await api('DELETE', `${base}/object?key=${encodeURIComponent(key)}`); toast('Deleted'); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function deleteFolder(p) {
|
||||
if (!(await confirmModal('Delete folder?', `Every object under ${p} will be deleted.`))) return;
|
||||
try { const r = await api('DELETE', `${base}/prefix?prefix=${encodeURIComponent(p)}`); toast(`Deleted ${r.deleted} objects`); load(); } catch (e) { fail(e); }
|
||||
}
|
||||
|
||||
async function share(key) {
|
||||
const expires = h('select', {}, [['3600', '1 hour'], ['86400', '1 day'], ['604800', '7 days (max)']].map(([v, l]) => h('option', { value: v }, l)));
|
||||
const savedEndpoint = localStorage.getItem('pithos.publicEndpoint') || '';
|
||||
const endpoint = h('input', { type: 'url', value: savedEndpoint, placeholder: `http://${state.status?.config.apiAddress}` });
|
||||
const out = h('div');
|
||||
const gen = async () => {
|
||||
const q = new URLSearchParams({ key, expires: expires.value });
|
||||
if (endpoint.value.trim()) { q.set('endpoint', endpoint.value.trim()); localStorage.setItem('pithos.publicEndpoint', endpoint.value.trim()); }
|
||||
try {
|
||||
const { url } = await api('GET', `${base}/presign?${q}`);
|
||||
put(out, secretRow('Link', url));
|
||||
} catch (e) { fail(e); }
|
||||
return false;
|
||||
};
|
||||
await modal('Share link', h('div', { class: 'stack' },
|
||||
h('p', { class: 'small muted', style: 'margin:0' }, 'A signed link that works without a key until it expires.'),
|
||||
h('label', { class: 'field' }, h('span', {}, 'Valid for'), expires),
|
||||
h('label', { class: 'field' }, h('span', {}, 'Public address of s3d'), endpoint, h('small', {}, 'The address recipients will use to reach s3d (e.g. behind a reverse proxy). Leave blank for the local endpoint.')),
|
||||
out),
|
||||
[{ label: 'Close', result: true }, { label: 'Create link', kind: 'primary', submit: true, value: gen }]);
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- settings
|
||||
|
||||
function settings(main) {
|
||||
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
||||
main.append(h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Settings'), h('p', { class: 'muted' }, 'Edits s3d.yml. s3d reads it at start, so a restart applies the changes.'))), card);
|
||||
|
||||
const fields = [
|
||||
['Network', [
|
||||
['apiAddress', 'S3 API address', 'text', 'host:port the S3 API listens on. Use 0.0.0.0:8000 to serve other machines.'],
|
||||
['apiHTTPSAddress', 'S3 HTTPS address', 'text', 'Optional. s3d serves a self-signed certificate here.'],
|
||||
['adminAddress', 'Admin API address', 'text', 'Pithos reads stats from here. Keep it on loopback.'],
|
||||
['adminPassword', 'Admin API password', 'password', 'Generated on first start.'],
|
||||
['s3.hostBases', 'Virtual-host bases', 'list', 'Comma-separated domains for bucket.domain style addressing.'],
|
||||
]],
|
||||
['Storage', [
|
||||
['directory', 'Data directory', 'text', 'Database and upload buffer. Changing it points s3d at a different store.'],
|
||||
['sia.diskUsageLimit', 'Buffer disk limit (bytes)', 'number', 'Upper bound for objects waiting to upload.'],
|
||||
['sia.uploadThreads', 'Upload threads', 'number', ''],
|
||||
['sia.dataShards', 'Data shards', 'number', 'Erasure coding: data shards (default 10). Applies to new uploads only.'],
|
||||
['sia.parityShards', 'Parity shards', 'number', 'Erasure coding: parity shards (default 20).'],
|
||||
]],
|
||||
['Logging', [
|
||||
['log.stdout.level', 'Console log level', 'select:debug,info,warn,error', ''],
|
||||
['log.file.enabled', 'Write a log file', 'bool', ''],
|
||||
['log.file.level', 'File log level', 'select:debug,info,warn,error', ''],
|
||||
]],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
let cfg;
|
||||
try { cfg = await api('GET', '/api/config'); } catch (e) { put(card, h('p', { class: 'error' }, e.message)); return; }
|
||||
const inputs = {};
|
||||
const sections = fields.map(([title, list]) => h('div', { class: 'stack', style: 'margin-bottom:22px' }, h('h2', {}, title), h('div', { class: 'form-grid' }, list.map(([key, label, type, help]) => {
|
||||
const v = cfg.values[key];
|
||||
let input;
|
||||
if (type === 'bool') input = h('input', { type: 'checkbox', checked: !!v });
|
||||
else if (type.startsWith('select:')) input = h('select', {}, h('option', { value: '' }, '(default)'), type.slice(7).split(',').map((o) => h('option', { value: o, selected: v === o }, o)));
|
||||
else input = h('input', { type: type === 'list' ? 'text' : type, value: Array.isArray(v) ? v.join(', ') : (v ?? ''), placeholder: '(default)', autocomplete: 'off' });
|
||||
inputs[key] = { input, type, orig: v };
|
||||
return type === 'bool'
|
||||
? h('label', { class: 'check' }, input, label)
|
||||
: h('label', { class: 'field' }, h('span', {}, label), input, help && h('small', {}, help));
|
||||
}))));
|
||||
put(card,
|
||||
h('p', { class: 'small muted' }, 'File: ', h('code', {}, cfg.file)),
|
||||
...sections,
|
||||
h('div', { class: 'row' }, h('button', { class: 'btn primary', onclick: save }, 'Save')));
|
||||
|
||||
async function save() {
|
||||
const patch = {};
|
||||
for (const [key, { input, type, orig }] of Object.entries(inputs)) {
|
||||
let v;
|
||||
if (type === 'bool') v = input.checked;
|
||||
else if (type === 'number') v = input.value === '' ? null : Number(input.value);
|
||||
else if (type === 'list') v = input.value.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
else v = input.value.trim() || null;
|
||||
const same = JSON.stringify(v ?? null) === JSON.stringify(orig ?? null) || (type === 'list' && !v.length && orig == null) || (type === 'bool' && !v && orig == null);
|
||||
if (!same) patch[key] = v;
|
||||
}
|
||||
if (!Object.keys(patch).length) return toast('Nothing changed');
|
||||
try {
|
||||
const r = await api('PUT', '/api/config', patch);
|
||||
for (const k of Object.keys(patch)) inputs[k].orig = patch[k];
|
||||
if (r.restartNeeded) {
|
||||
if (await confirmModal('Restart s3d?', 'The new settings apply after a restart.', 'Restart now', 'primary')) await daemonAction('restart');
|
||||
} else toast('Saved');
|
||||
refreshStatus();
|
||||
} catch (e) { fail(e); }
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- logs
|
||||
|
||||
function logs(main) {
|
||||
const box = h('div', { class: 'logs' });
|
||||
let follow = true;
|
||||
box.addEventListener('scroll', () => { follow = box.scrollTop + box.clientHeight >= box.scrollHeight - 20; });
|
||||
main.append(
|
||||
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Logs'), h('p', { class: 'muted' }, 'Output from the s3d process Pithos started.')),
|
||||
h('button', { class: 'btn', onclick: () => copy(state.logs.map((l) => l.line).join('\n'), 'Logs copied') }, 'Copy all')),
|
||||
box);
|
||||
const line = (l) => h('div', { class: l.stream }, l.line);
|
||||
box.append(...state.logs.map(line));
|
||||
if (!state.logs.length) box.append(h('div', { class: 'sys' }, 'No output yet.'));
|
||||
box.scrollTop = box.scrollHeight;
|
||||
logSink = (l) => {
|
||||
box.append(line(l));
|
||||
while (box.childElementCount > 2000) box.firstChild.remove();
|
||||
if (follow) box.scrollTop = box.scrollHeight;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- boot
|
||||
|
||||
function showSignin() {
|
||||
$('#app').hidden = true;
|
||||
$('#signin').hidden = false;
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
const s = await api('GET', '/api/session');
|
||||
if (!s.authenticated) {
|
||||
showSignin();
|
||||
if (!s.passwordLogin) {
|
||||
$('#signin-hint').textContent = 'Open Pithos with the sign-in link printed when it started.';
|
||||
$('#signin-form').querySelector('input').hidden = true;
|
||||
$('#signin-form').querySelector('button').hidden = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
$('#signin').hidden = true;
|
||||
$('#app').hidden = false;
|
||||
// Backfill logs the server already holds, then stream.
|
||||
try {
|
||||
state.logs = await api('GET', '/api/logs');
|
||||
state.logSeq = state.logs.at(-1)?.seq || 0;
|
||||
} catch {}
|
||||
await refreshStatus();
|
||||
connectEvents();
|
||||
window.addEventListener('hashchange', route);
|
||||
route();
|
||||
setInterval(refreshStatus, 15000);
|
||||
}
|
||||
|
||||
$('#signin-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const err = $('#signin-error');
|
||||
err.hidden = true;
|
||||
try {
|
||||
await api('POST', '/api/session', { password: $('#signin-password').value });
|
||||
location.reload();
|
||||
} catch (ex) { err.textContent = ex.message; err.hidden = false; }
|
||||
});
|
||||
|
||||
boot().catch(fail);
|
||||
1
bundled-addons/pithos/ui/icon.svg
Normal file
1
bundled-addons/pithos/ui/icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3ddc97"/><stop offset="1" stop-color="#1e8f6a"/></linearGradient></defs><path d="M22 6h20v5c0 2-2 3-2 5 8 3 14 11 14 21 0 12-9 21-22 21S10 49 10 37c0-10 6-18 14-21 0-2-2-3-2-5z" fill="url(#g)"/><path d="M14 30h36M13 40h38" stroke="#0b1f18" stroke-width="3" stroke-linecap="round" opacity=".55"/></svg>
|
||||
|
After Width: | Height: | Size: 449 B |
42
bundled-addons/pithos/ui/index.html
Normal file
42
bundled-addons/pithos/ui/index.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pithos</title>
|
||||
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="signin" class="signin" hidden>
|
||||
<form class="card signin-card" id="signin-form">
|
||||
<img src="icon.svg" alt="" width="48" height="48">
|
||||
<h1>Pithos</h1>
|
||||
<p class="muted" id="signin-hint">Enter the console password.</p>
|
||||
<input type="password" id="signin-password" autocomplete="current-password" placeholder="Password" required>
|
||||
<button class="btn primary" type="submit">Sign in</button>
|
||||
<p class="error" id="signin-error" hidden></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="app" class="app" hidden>
|
||||
<aside class="sidebar">
|
||||
<div class="brand"><img src="icon.svg" alt="" width="28" height="28"><span>Pithos</span></div>
|
||||
<nav id="nav">
|
||||
<a href="#/overview" data-view="overview">Overview</a>
|
||||
<a href="#/buckets" data-view="buckets">Buckets</a>
|
||||
<a href="#/users" data-view="users">Users & keys</a>
|
||||
<a href="#/setup" data-view="setup">Sia connection</a>
|
||||
<a href="#/settings" data-view="settings">Settings</a>
|
||||
<a href="#/logs" data-view="logs">Logs</a>
|
||||
</nav>
|
||||
<div class="daemon-pill" id="daemon-pill"><span class="dot"></span><span class="label">…</span></div>
|
||||
</aside>
|
||||
<main id="main"></main>
|
||||
</div>
|
||||
|
||||
<div id="modal-root"></div>
|
||||
<div id="toasts" class="toasts" aria-live="polite"></div>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
bundled-addons/pithos/vendor/yaml/LICENSE
vendored
Normal file
13
bundled-addons/pithos/vendor/yaml/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Copyright Eemeli Aro <eemeli@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
201
bundled-addons/pithos/vendor/yaml/lib/cli.mjs
vendored
Normal file
201
bundled-addons/pithos/vendor/yaml/lib/cli.mjs
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { resolve } from 'path';
|
||||
import { parseArgs } from 'util';
|
||||
import { prettyToken } from './parse/cst.js';
|
||||
import { Lexer } from './parse/lexer.js';
|
||||
import { Parser } from './parse/parser.js';
|
||||
import { Composer } from './compose/composer.js';
|
||||
import { LineCounter } from './parse/line-counter.js';
|
||||
import { prettifyError } from './errors.js';
|
||||
import { visit } from './visit.js';
|
||||
|
||||
const help = `\
|
||||
yaml: A command-line YAML processor and inspector
|
||||
|
||||
Reads stdin and writes output to stdout and errors & warnings to stderr.
|
||||
|
||||
Usage:
|
||||
yaml Process a YAML stream, outputting it as YAML
|
||||
yaml cst Parse the CST of a YAML stream
|
||||
yaml lex Parse the lexical tokens of a YAML stream
|
||||
yaml valid Validate a YAML stream, returning 0 on success
|
||||
|
||||
Options:
|
||||
--help, -h Show this message.
|
||||
--json, -j Output JSON.
|
||||
--indent 2 Output pretty-printed data, indented by the given number of spaces.
|
||||
--merge, -m Enable support for "<<" merge keys.
|
||||
|
||||
Additional options for bare "yaml" command:
|
||||
--doc, -d Output pretty-printed JS Document objects.
|
||||
--single, -1 Require the input to consist of a single YAML document.
|
||||
--strict, -s Stop on errors.
|
||||
--visit, -v Apply a visitor to each document (requires a path to import)
|
||||
--yaml 1.1 Set the YAML version. (default: 1.2)`;
|
||||
class UserError extends Error {
|
||||
constructor(code, message) {
|
||||
super(`Error: ${message}`);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
UserError.ARGS = 2;
|
||||
UserError.SINGLE = 3;
|
||||
async function cli(stdin, done, argv) {
|
||||
let args;
|
||||
try {
|
||||
args = parseArgs({
|
||||
args: argv,
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
doc: { type: 'boolean', short: 'd' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
indent: { type: 'string', short: 'i' },
|
||||
merge: { type: 'boolean', short: 'm' },
|
||||
json: { type: 'boolean', short: 'j' },
|
||||
single: { type: 'boolean', short: '1' },
|
||||
strict: { type: 'boolean', short: 's' },
|
||||
visit: { type: 'string', short: 'v' },
|
||||
yaml: { type: 'string', default: '1.2' }
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return done(new UserError(UserError.ARGS, error.message));
|
||||
}
|
||||
const { positionals: [mode], values: opt } = args;
|
||||
let indent = Number(opt.indent);
|
||||
stdin.setEncoding('utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
switch (opt.help || mode) {
|
||||
/* istanbul ignore next */
|
||||
case true: // --help
|
||||
console.log(help);
|
||||
break;
|
||||
case 'lex': {
|
||||
const lexer = new Lexer();
|
||||
const data = [];
|
||||
const add = (tok) => {
|
||||
if (opt.json)
|
||||
data.push(tok);
|
||||
else
|
||||
console.log(prettyToken(tok));
|
||||
};
|
||||
stdin.on('data', (chunk) => {
|
||||
for (const tok of lexer.lex(chunk, true))
|
||||
add(tok);
|
||||
});
|
||||
stdin.on('end', () => {
|
||||
for (const tok of lexer.lex('', false))
|
||||
add(tok);
|
||||
if (opt.json)
|
||||
console.log(JSON.stringify(data, null, indent));
|
||||
done();
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'cst': {
|
||||
const parser = new Parser();
|
||||
const data = [];
|
||||
const add = (tok) => {
|
||||
if (opt.json)
|
||||
data.push(tok);
|
||||
else
|
||||
console.dir(tok, { depth: null });
|
||||
};
|
||||
stdin.on('data', (chunk) => {
|
||||
for (const tok of parser.parse(chunk, true))
|
||||
add(tok);
|
||||
});
|
||||
stdin.on('end', () => {
|
||||
for (const tok of parser.parse('', false))
|
||||
add(tok);
|
||||
if (opt.json)
|
||||
console.log(JSON.stringify(data, null, indent));
|
||||
done();
|
||||
});
|
||||
break;
|
||||
}
|
||||
case undefined:
|
||||
case 'valid': {
|
||||
const lineCounter = new LineCounter();
|
||||
const parser = new Parser(lineCounter.addNewLine);
|
||||
// @ts-expect-error Version is validated at runtime
|
||||
const composer = new Composer({ version: opt.yaml, merge: opt.merge });
|
||||
const visitor = opt.visit
|
||||
? (await import(resolve(opt.visit))).default
|
||||
: null;
|
||||
let source = '';
|
||||
let hasDoc = false;
|
||||
let reqDocEnd = false;
|
||||
const data = [];
|
||||
const add = (doc) => {
|
||||
if (hasDoc && opt.single) {
|
||||
return done(new UserError(UserError.SINGLE, 'Input stream contains multiple documents'));
|
||||
}
|
||||
for (const error of doc.errors) {
|
||||
prettifyError(source, lineCounter)(error);
|
||||
if (opt.strict || mode === 'valid')
|
||||
return done(error);
|
||||
console.error(error);
|
||||
}
|
||||
for (const warning of doc.warnings) {
|
||||
prettifyError(source, lineCounter)(warning);
|
||||
console.error(warning);
|
||||
}
|
||||
if (visitor)
|
||||
visit(doc, visitor);
|
||||
if (mode === 'valid')
|
||||
doc.toJS();
|
||||
else if (opt.json)
|
||||
data.push(doc);
|
||||
else if (opt.doc) {
|
||||
Object.defineProperties(doc, {
|
||||
options: { enumerable: false },
|
||||
schema: { enumerable: false }
|
||||
});
|
||||
console.dir(doc, { depth: null });
|
||||
}
|
||||
else {
|
||||
if (reqDocEnd)
|
||||
console.log('...');
|
||||
try {
|
||||
indent || (indent = 2);
|
||||
const str = doc.toString({ indent });
|
||||
console.log(str.endsWith('\n') ? str.slice(0, -1) : str);
|
||||
}
|
||||
catch (error) {
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
hasDoc = true;
|
||||
reqDocEnd = !doc.directives?.docEnd;
|
||||
};
|
||||
stdin.on('data', (chunk) => {
|
||||
source += chunk;
|
||||
for (const tok of parser.parse(chunk, true)) {
|
||||
for (const doc of composer.next(tok))
|
||||
add(doc);
|
||||
}
|
||||
});
|
||||
stdin.on('end', () => {
|
||||
for (const tok of parser.parse('', false)) {
|
||||
for (const doc of composer.next(tok))
|
||||
add(doc);
|
||||
}
|
||||
for (const doc of composer.end(false))
|
||||
add(doc);
|
||||
if (opt.single && !hasDoc) {
|
||||
return done(new UserError(UserError.SINGLE, 'Input stream contained no documents'));
|
||||
}
|
||||
if (mode !== 'valid' && opt.json) {
|
||||
console.log(JSON.stringify(opt.single ? data[0] : data, null, indent));
|
||||
}
|
||||
done();
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
done(new UserError(UserError.ARGS, `Unknown command: ${JSON.stringify(mode)}`));
|
||||
}
|
||||
}
|
||||
|
||||
export { UserError, cli, help };
|
||||
90
bundled-addons/pithos/vendor/yaml/lib/compose/compose-collection.js
vendored
Normal file
90
bundled-addons/pithos/vendor/yaml/lib/compose/compose-collection.js
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
var YAMLMap = require('../nodes/YAMLMap.js');
|
||||
var YAMLSeq = require('../nodes/YAMLSeq.js');
|
||||
var resolveBlockMap = require('./resolve-block-map.js');
|
||||
var resolveBlockSeq = require('./resolve-block-seq.js');
|
||||
var resolveFlowCollection = require('./resolve-flow-collection.js');
|
||||
|
||||
function resolveCollection(CN, ctx, token, onError, tagName, tag) {
|
||||
const coll = token.type === 'block-map'
|
||||
? resolveBlockMap.resolveBlockMap(CN, ctx, token, onError, tag)
|
||||
: token.type === 'block-seq'
|
||||
? resolveBlockSeq.resolveBlockSeq(CN, ctx, token, onError, tag)
|
||||
: resolveFlowCollection.resolveFlowCollection(CN, ctx, token, onError, tag);
|
||||
const Coll = coll.constructor;
|
||||
// If we got a tagName matching the class, or the tag name is '!',
|
||||
// then use the tagName from the node class used to create it.
|
||||
if (tagName === '!' || tagName === Coll.tagName) {
|
||||
coll.tag = Coll.tagName;
|
||||
return coll;
|
||||
}
|
||||
if (tagName)
|
||||
coll.tag = tagName;
|
||||
return coll;
|
||||
}
|
||||
function composeCollection(CN, ctx, token, props, onError) {
|
||||
const tagToken = props.tag;
|
||||
const tagName = !tagToken
|
||||
? null
|
||||
: ctx.directives.tagName(tagToken.source, msg => onError(tagToken, 'TAG_RESOLVE_FAILED', msg));
|
||||
if (token.type === 'block-seq') {
|
||||
const { anchor, newlineAfterProp: nl } = props;
|
||||
const lastProp = anchor && tagToken
|
||||
? anchor.offset > tagToken.offset
|
||||
? anchor
|
||||
: tagToken
|
||||
: (anchor ?? tagToken);
|
||||
if (lastProp && (!nl || nl.offset < lastProp.offset)) {
|
||||
const message = 'Missing newline after block sequence props';
|
||||
onError(lastProp, 'MISSING_CHAR', message);
|
||||
}
|
||||
}
|
||||
const expType = token.type === 'block-map'
|
||||
? 'map'
|
||||
: token.type === 'block-seq'
|
||||
? 'seq'
|
||||
: token.start.source === '{'
|
||||
? 'map'
|
||||
: 'seq';
|
||||
// shortcut: check if it's a generic YAMLMap or YAMLSeq
|
||||
// before jumping into the custom tag logic.
|
||||
if (!tagToken ||
|
||||
!tagName ||
|
||||
tagName === '!' ||
|
||||
(tagName === YAMLMap.YAMLMap.tagName && expType === 'map') ||
|
||||
(tagName === YAMLSeq.YAMLSeq.tagName && expType === 'seq')) {
|
||||
return resolveCollection(CN, ctx, token, onError, tagName);
|
||||
}
|
||||
let tag = ctx.schema.tags.find(t => t.tag === tagName && t.collection === expType);
|
||||
if (!tag) {
|
||||
const kt = ctx.schema.knownTags[tagName];
|
||||
if (kt?.collection === expType) {
|
||||
ctx.schema.tags.push(Object.assign({}, kt, { default: false }));
|
||||
tag = kt;
|
||||
}
|
||||
else {
|
||||
if (kt) {
|
||||
onError(tagToken, 'BAD_COLLECTION_TYPE', `${kt.tag} used for ${expType} collection, but expects ${kt.collection ?? 'scalar'}`, true);
|
||||
}
|
||||
else {
|
||||
onError(tagToken, 'TAG_RESOLVE_FAILED', `Unresolved tag: ${tagName}`, true);
|
||||
}
|
||||
return resolveCollection(CN, ctx, token, onError, tagName);
|
||||
}
|
||||
}
|
||||
const coll = resolveCollection(CN, ctx, token, onError, tagName, tag);
|
||||
const res = tag.resolve?.(coll, msg => onError(tagToken, 'TAG_RESOLVE_FAILED', msg), ctx.options) ?? coll;
|
||||
const node = identity.isNode(res)
|
||||
? res
|
||||
: new Scalar.Scalar(res);
|
||||
node.range = coll.range;
|
||||
node.tag = tagName;
|
||||
if (tag?.format)
|
||||
node.format = tag.format;
|
||||
return node;
|
||||
}
|
||||
|
||||
exports.composeCollection = composeCollection;
|
||||
45
bundled-addons/pithos/vendor/yaml/lib/compose/compose-doc.js
vendored
Normal file
45
bundled-addons/pithos/vendor/yaml/lib/compose/compose-doc.js
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
'use strict';
|
||||
|
||||
var Document = require('../doc/Document.js');
|
||||
var composeNode = require('./compose-node.js');
|
||||
var resolveEnd = require('./resolve-end.js');
|
||||
var resolveProps = require('./resolve-props.js');
|
||||
|
||||
function composeDoc(options, directives, { offset, start, value, end }, onError) {
|
||||
const opts = Object.assign({ _directives: directives }, options);
|
||||
const doc = new Document.Document(undefined, opts);
|
||||
const ctx = {
|
||||
atKey: false,
|
||||
atRoot: true,
|
||||
directives: doc.directives,
|
||||
options: doc.options,
|
||||
schema: doc.schema
|
||||
};
|
||||
const props = resolveProps.resolveProps(start, {
|
||||
indicator: 'doc-start',
|
||||
next: value ?? end?.[0],
|
||||
offset,
|
||||
onError,
|
||||
parentIndent: 0,
|
||||
startOnNewline: true
|
||||
});
|
||||
if (props.found) {
|
||||
doc.directives.docStart = true;
|
||||
if (value &&
|
||||
(value.type === 'block-map' || value.type === 'block-seq') &&
|
||||
!props.hasNewline)
|
||||
onError(props.end, 'MISSING_CHAR', 'Block collection cannot start on same line with directives-end marker');
|
||||
}
|
||||
// @ts-expect-error If Contents is set, let's trust the user
|
||||
doc.contents = value
|
||||
? composeNode.composeNode(ctx, value, props, onError)
|
||||
: composeNode.composeEmptyNode(ctx, props.end, start, null, props, onError);
|
||||
const contentEnd = doc.contents.range[2];
|
||||
const re = resolveEnd.resolveEnd(end, contentEnd, false, onError);
|
||||
if (re.comment)
|
||||
doc.comment = re.comment;
|
||||
doc.range = [offset, contentEnd, re.offset];
|
||||
return doc;
|
||||
}
|
||||
|
||||
exports.composeDoc = composeDoc;
|
||||
112
bundled-addons/pithos/vendor/yaml/lib/compose/compose-node.js
vendored
Normal file
112
bundled-addons/pithos/vendor/yaml/lib/compose/compose-node.js
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
'use strict';
|
||||
|
||||
var Alias = require('../nodes/Alias.js');
|
||||
var identity = require('../nodes/identity.js');
|
||||
var composeCollection = require('./compose-collection.js');
|
||||
var composeScalar = require('./compose-scalar.js');
|
||||
var resolveEnd = require('./resolve-end.js');
|
||||
var utilEmptyScalarPosition = require('./util-empty-scalar-position.js');
|
||||
|
||||
const CN = { composeNode, composeEmptyNode };
|
||||
function composeNode(ctx, token, props, onError) {
|
||||
const atKey = ctx.atKey;
|
||||
const { spaceBefore, comment, anchor, tag } = props;
|
||||
let node;
|
||||
let isSrcToken = true;
|
||||
switch (token.type) {
|
||||
case 'alias':
|
||||
node = composeAlias(ctx, token, onError);
|
||||
if (anchor || tag)
|
||||
onError(token, 'ALIAS_PROPS', 'An alias node must not specify any properties');
|
||||
break;
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar':
|
||||
case 'block-scalar':
|
||||
node = composeScalar.composeScalar(ctx, token, tag, onError);
|
||||
if (anchor)
|
||||
node.anchor = anchor.source.substring(1);
|
||||
break;
|
||||
case 'block-map':
|
||||
case 'block-seq':
|
||||
case 'flow-collection':
|
||||
try {
|
||||
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
|
||||
if (anchor)
|
||||
node.anchor = anchor.source.substring(1);
|
||||
}
|
||||
catch (error) {
|
||||
// Almost certainly here due to a stack overflow
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
onError(token, 'RESOURCE_EXHAUSTION', message);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const message = token.type === 'error'
|
||||
? token.message
|
||||
: `Unsupported token (type: ${token.type})`;
|
||||
onError(token, 'UNEXPECTED_TOKEN', message);
|
||||
isSrcToken = false;
|
||||
}
|
||||
}
|
||||
node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
|
||||
if (anchor && node.anchor === '')
|
||||
onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
|
||||
if (atKey &&
|
||||
ctx.options.stringKeys &&
|
||||
(!identity.isScalar(node) ||
|
||||
typeof node.value !== 'string' ||
|
||||
(node.tag && node.tag !== 'tag:yaml.org,2002:str'))) {
|
||||
const msg = 'With stringKeys, all keys must be strings';
|
||||
onError(tag ?? token, 'NON_STRING_KEY', msg);
|
||||
}
|
||||
if (spaceBefore)
|
||||
node.spaceBefore = true;
|
||||
if (comment) {
|
||||
if (token.type === 'scalar' && token.source === '')
|
||||
node.comment = comment;
|
||||
else
|
||||
node.commentBefore = comment;
|
||||
}
|
||||
// @ts-expect-error Type checking misses meaning of isSrcToken
|
||||
if (ctx.options.keepSourceTokens && isSrcToken)
|
||||
node.srcToken = token;
|
||||
return node;
|
||||
}
|
||||
function composeEmptyNode(ctx, offset, before, pos, { spaceBefore, comment, anchor, tag, end }, onError) {
|
||||
const token = {
|
||||
type: 'scalar',
|
||||
offset: utilEmptyScalarPosition.emptyScalarPosition(offset, before, pos),
|
||||
indent: -1,
|
||||
source: ''
|
||||
};
|
||||
const node = composeScalar.composeScalar(ctx, token, tag, onError);
|
||||
if (anchor) {
|
||||
node.anchor = anchor.source.substring(1);
|
||||
if (node.anchor === '')
|
||||
onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
|
||||
}
|
||||
if (spaceBefore)
|
||||
node.spaceBefore = true;
|
||||
if (comment) {
|
||||
node.comment = comment;
|
||||
node.range[2] = end;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
function composeAlias({ options }, { offset, source, end }, onError) {
|
||||
const alias = new Alias.Alias(source.substring(1));
|
||||
if (alias.source === '')
|
||||
onError(offset, 'BAD_ALIAS', 'Alias cannot be an empty string');
|
||||
if (alias.source.endsWith(':'))
|
||||
onError(offset + source.length - 1, 'BAD_ALIAS', 'Alias ending in : is ambiguous', true);
|
||||
const valueEnd = offset + source.length;
|
||||
const re = resolveEnd.resolveEnd(end, valueEnd, options.strict, onError);
|
||||
alias.range = [offset, valueEnd, re.offset];
|
||||
if (re.comment)
|
||||
alias.comment = re.comment;
|
||||
return alias;
|
||||
}
|
||||
|
||||
exports.composeEmptyNode = composeEmptyNode;
|
||||
exports.composeNode = composeNode;
|
||||
88
bundled-addons/pithos/vendor/yaml/lib/compose/compose-scalar.js
vendored
Normal file
88
bundled-addons/pithos/vendor/yaml/lib/compose/compose-scalar.js
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
var resolveBlockScalar = require('./resolve-block-scalar.js');
|
||||
var resolveFlowScalar = require('./resolve-flow-scalar.js');
|
||||
|
||||
function composeScalar(ctx, token, tagToken, onError) {
|
||||
const { value, type, comment, range } = token.type === 'block-scalar'
|
||||
? resolveBlockScalar.resolveBlockScalar(ctx, token, onError)
|
||||
: resolveFlowScalar.resolveFlowScalar(token, ctx.options.strict, onError);
|
||||
const tagName = tagToken
|
||||
? ctx.directives.tagName(tagToken.source, msg => onError(tagToken, 'TAG_RESOLVE_FAILED', msg))
|
||||
: null;
|
||||
let tag;
|
||||
if (ctx.options.stringKeys && ctx.atKey) {
|
||||
tag = ctx.schema[identity.SCALAR];
|
||||
}
|
||||
else if (tagName)
|
||||
tag = findScalarTagByName(ctx.schema, value, tagName, tagToken, onError);
|
||||
else if (token.type === 'scalar')
|
||||
tag = findScalarTagByTest(ctx, value, token, onError);
|
||||
else
|
||||
tag = ctx.schema[identity.SCALAR];
|
||||
let scalar;
|
||||
try {
|
||||
const res = tag.resolve(value, msg => onError(tagToken ?? token, 'TAG_RESOLVE_FAILED', msg), ctx.options);
|
||||
scalar = identity.isScalar(res) ? res : new Scalar.Scalar(res);
|
||||
}
|
||||
catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
onError(tagToken ?? token, 'TAG_RESOLVE_FAILED', msg);
|
||||
scalar = new Scalar.Scalar(value);
|
||||
}
|
||||
scalar.range = range;
|
||||
scalar.source = value;
|
||||
if (type)
|
||||
scalar.type = type;
|
||||
if (tagName)
|
||||
scalar.tag = tagName;
|
||||
if (tag.format)
|
||||
scalar.format = tag.format;
|
||||
if (comment)
|
||||
scalar.comment = comment;
|
||||
return scalar;
|
||||
}
|
||||
function findScalarTagByName(schema, value, tagName, tagToken, onError) {
|
||||
if (tagName === '!')
|
||||
return schema[identity.SCALAR]; // non-specific tag
|
||||
const matchWithTest = [];
|
||||
for (const tag of schema.tags) {
|
||||
if (!tag.collection && tag.tag === tagName) {
|
||||
if (tag.default && tag.test)
|
||||
matchWithTest.push(tag);
|
||||
else
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
for (const tag of matchWithTest)
|
||||
if (tag.test?.test(value))
|
||||
return tag;
|
||||
const kt = schema.knownTags[tagName];
|
||||
if (kt && !kt.collection) {
|
||||
// Ensure that the known tag is available for stringifying,
|
||||
// but does not get used by default.
|
||||
schema.tags.push(Object.assign({}, kt, { default: false, test: undefined }));
|
||||
return kt;
|
||||
}
|
||||
onError(tagToken, 'TAG_RESOLVE_FAILED', `Unresolved tag: ${tagName}`, tagName !== 'tag:yaml.org,2002:str');
|
||||
return schema[identity.SCALAR];
|
||||
}
|
||||
function findScalarTagByTest({ atKey, directives, schema }, value, token, onError) {
|
||||
const tag = schema.tags.find(tag => (tag.default === true || (atKey && tag.default === 'key')) &&
|
||||
tag.test?.test(value)) || schema[identity.SCALAR];
|
||||
if (schema.compat) {
|
||||
const compat = schema.compat.find(tag => tag.default && tag.test?.test(value)) ??
|
||||
schema[identity.SCALAR];
|
||||
if (tag.tag !== compat.tag) {
|
||||
const ts = directives.tagString(tag.tag);
|
||||
const cs = directives.tagString(compat.tag);
|
||||
const msg = `Value may be parsed as either ${ts} or ${cs}`;
|
||||
onError(token, 'TAG_RESOLVE_FAILED', msg, true);
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
exports.composeScalar = composeScalar;
|
||||
224
bundled-addons/pithos/vendor/yaml/lib/compose/composer.js
vendored
Normal file
224
bundled-addons/pithos/vendor/yaml/lib/compose/composer.js
vendored
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
'use strict';
|
||||
|
||||
var node_process = require('process');
|
||||
var directives = require('../doc/directives.js');
|
||||
var Document = require('../doc/Document.js');
|
||||
var errors = require('../errors.js');
|
||||
var identity = require('../nodes/identity.js');
|
||||
var composeDoc = require('./compose-doc.js');
|
||||
var resolveEnd = require('./resolve-end.js');
|
||||
|
||||
function getErrorPos(src) {
|
||||
if (typeof src === 'number')
|
||||
return [src, src + 1];
|
||||
if (Array.isArray(src))
|
||||
return src.length === 2 ? src : [src[0], src[1]];
|
||||
const { offset, source } = src;
|
||||
return [offset, offset + (typeof source === 'string' ? source.length : 1)];
|
||||
}
|
||||
function parsePrelude(prelude) {
|
||||
let comment = '';
|
||||
let atComment = false;
|
||||
let afterEmptyLine = false;
|
||||
for (let i = 0; i < prelude.length; ++i) {
|
||||
const source = prelude[i];
|
||||
switch (source[0]) {
|
||||
case '#':
|
||||
comment +=
|
||||
(comment === '' ? '' : afterEmptyLine ? '\n\n' : '\n') +
|
||||
(source.substring(1) || ' ');
|
||||
atComment = true;
|
||||
afterEmptyLine = false;
|
||||
break;
|
||||
case '%':
|
||||
if (prelude[i + 1]?.[0] !== '#')
|
||||
i += 1;
|
||||
atComment = false;
|
||||
break;
|
||||
default:
|
||||
// This may be wrong after doc-end, but in that case it doesn't matter
|
||||
if (!atComment)
|
||||
afterEmptyLine = true;
|
||||
atComment = false;
|
||||
}
|
||||
}
|
||||
return { comment, afterEmptyLine };
|
||||
}
|
||||
/**
|
||||
* Compose a stream of CST nodes into a stream of YAML Documents.
|
||||
*
|
||||
* ```ts
|
||||
* import { Composer, Parser } from 'yaml'
|
||||
*
|
||||
* const src: string = ...
|
||||
* const tokens = new Parser().parse(src)
|
||||
* const docs = new Composer().compose(tokens)
|
||||
* ```
|
||||
*/
|
||||
class Composer {
|
||||
constructor(options = {}) {
|
||||
this.doc = null;
|
||||
this.atDirectives = false;
|
||||
this.prelude = [];
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
this.onError = (source, code, message, warning) => {
|
||||
const pos = getErrorPos(source);
|
||||
if (warning)
|
||||
this.warnings.push(new errors.YAMLWarning(pos, code, message));
|
||||
else
|
||||
this.errors.push(new errors.YAMLParseError(pos, code, message));
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
this.directives = new directives.Directives({ version: options.version || '1.2' });
|
||||
this.options = options;
|
||||
}
|
||||
decorate(doc, afterDoc) {
|
||||
const { comment, afterEmptyLine } = parsePrelude(this.prelude);
|
||||
//console.log({ dc: doc.comment, prelude, comment })
|
||||
if (comment) {
|
||||
const dc = doc.contents;
|
||||
if (afterDoc) {
|
||||
doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment;
|
||||
}
|
||||
else if (afterEmptyLine || doc.directives.docStart || !dc) {
|
||||
doc.commentBefore = comment;
|
||||
}
|
||||
else if (identity.isCollection(dc) && !dc.flow && dc.items.length > 0) {
|
||||
let it = dc.items[0];
|
||||
if (identity.isPair(it))
|
||||
it = it.key;
|
||||
const cb = it.commentBefore;
|
||||
it.commentBefore = cb ? `${comment}\n${cb}` : comment;
|
||||
}
|
||||
else {
|
||||
const cb = dc.commentBefore;
|
||||
dc.commentBefore = cb ? `${comment}\n${cb}` : comment;
|
||||
}
|
||||
}
|
||||
if (afterDoc) {
|
||||
for (let i = 0; i < this.errors.length; ++i)
|
||||
doc.errors.push(this.errors[i]);
|
||||
for (let i = 0; i < this.warnings.length; ++i)
|
||||
doc.warnings.push(this.warnings[i]);
|
||||
}
|
||||
else {
|
||||
doc.errors = this.errors;
|
||||
doc.warnings = this.warnings;
|
||||
}
|
||||
this.prelude = [];
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
}
|
||||
/**
|
||||
* Current stream status information.
|
||||
*
|
||||
* Mostly useful at the end of input for an empty stream.
|
||||
*/
|
||||
streamInfo() {
|
||||
return {
|
||||
comment: parsePrelude(this.prelude).comment,
|
||||
directives: this.directives,
|
||||
errors: this.errors,
|
||||
warnings: this.warnings
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Compose tokens into documents.
|
||||
*
|
||||
* @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
|
||||
* @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
|
||||
*/
|
||||
*compose(tokens, forceDoc = false, endOffset = -1) {
|
||||
for (const token of tokens)
|
||||
yield* this.next(token);
|
||||
yield* this.end(forceDoc, endOffset);
|
||||
}
|
||||
/** Advance the composer by one CST token. */
|
||||
*next(token) {
|
||||
if (node_process.env.LOG_STREAM)
|
||||
console.dir(token, { depth: null });
|
||||
switch (token.type) {
|
||||
case 'directive':
|
||||
this.directives.add(token.source, (offset, message, warning) => {
|
||||
const pos = getErrorPos(token);
|
||||
pos[0] += offset;
|
||||
this.onError(pos, 'BAD_DIRECTIVE', message, warning);
|
||||
});
|
||||
this.prelude.push(token.source);
|
||||
this.atDirectives = true;
|
||||
break;
|
||||
case 'document': {
|
||||
const doc = composeDoc.composeDoc(this.options, this.directives, token, this.onError);
|
||||
if (this.atDirectives && !doc.directives.docStart)
|
||||
this.onError(token, 'MISSING_CHAR', 'Missing directives-end/doc-start indicator line');
|
||||
this.decorate(doc, false);
|
||||
if (this.doc)
|
||||
yield this.doc;
|
||||
this.doc = doc;
|
||||
this.atDirectives = false;
|
||||
break;
|
||||
}
|
||||
case 'byte-order-mark':
|
||||
case 'space':
|
||||
break;
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
this.prelude.push(token.source);
|
||||
break;
|
||||
case 'error': {
|
||||
const msg = token.source
|
||||
? `${token.message}: ${JSON.stringify(token.source)}`
|
||||
: token.message;
|
||||
const error = new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg);
|
||||
if (this.atDirectives || !this.doc)
|
||||
this.errors.push(error);
|
||||
else
|
||||
this.doc.errors.push(error);
|
||||
break;
|
||||
}
|
||||
case 'doc-end': {
|
||||
if (!this.doc) {
|
||||
const msg = 'Unexpected doc-end without preceding document';
|
||||
this.errors.push(new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg));
|
||||
break;
|
||||
}
|
||||
this.doc.directives.docEnd = true;
|
||||
const end = resolveEnd.resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError);
|
||||
this.decorate(this.doc, true);
|
||||
if (end.comment) {
|
||||
const dc = this.doc.comment;
|
||||
this.doc.comment = dc ? `${dc}\n${end.comment}` : end.comment;
|
||||
}
|
||||
this.doc.range[2] = end.offset;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.errors.push(new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', `Unsupported token ${token.type}`));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call at end of input to yield any remaining document.
|
||||
*
|
||||
* @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
|
||||
* @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
|
||||
*/
|
||||
*end(forceDoc = false, endOffset = -1) {
|
||||
if (this.doc) {
|
||||
this.decorate(this.doc, true);
|
||||
yield this.doc;
|
||||
this.doc = null;
|
||||
}
|
||||
else if (forceDoc) {
|
||||
const opts = Object.assign({ _directives: this.directives }, this.options);
|
||||
const doc = new Document.Document(undefined, opts);
|
||||
if (this.atDirectives)
|
||||
this.onError(endOffset, 'MISSING_CHAR', 'Missing directives-end indicator line');
|
||||
doc.range = [0, endOffset, endOffset];
|
||||
this.decorate(doc, false);
|
||||
yield doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Composer = Composer;
|
||||
117
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-map.js
vendored
Normal file
117
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-map.js
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
'use strict';
|
||||
|
||||
var Pair = require('../nodes/Pair.js');
|
||||
var YAMLMap = require('../nodes/YAMLMap.js');
|
||||
var resolveProps = require('./resolve-props.js');
|
||||
var utilContainsNewline = require('./util-contains-newline.js');
|
||||
var utilFlowIndentCheck = require('./util-flow-indent-check.js');
|
||||
var utilMapIncludes = require('./util-map-includes.js');
|
||||
|
||||
const startColMsg = 'All mapping items must start at the same column';
|
||||
function resolveBlockMap({ composeNode, composeEmptyNode }, ctx, bm, onError, tag) {
|
||||
const NodeClass = tag?.nodeClass ?? YAMLMap.YAMLMap;
|
||||
const map = new NodeClass(ctx.schema);
|
||||
if (ctx.atRoot)
|
||||
ctx.atRoot = false;
|
||||
let offset = bm.offset;
|
||||
let commentEnd = null;
|
||||
for (const collItem of bm.items) {
|
||||
const { start, key, sep, value } = collItem;
|
||||
// key properties
|
||||
const keyProps = resolveProps.resolveProps(start, {
|
||||
indicator: 'explicit-key-ind',
|
||||
next: key ?? sep?.[0],
|
||||
offset,
|
||||
onError,
|
||||
parentIndent: bm.indent,
|
||||
startOnNewline: true
|
||||
});
|
||||
const implicitKey = !keyProps.found;
|
||||
if (implicitKey) {
|
||||
if (key) {
|
||||
if (key.type === 'block-seq')
|
||||
onError(offset, 'BLOCK_AS_IMPLICIT_KEY', 'A block sequence may not be used as an implicit map key');
|
||||
else if ('indent' in key && key.indent !== bm.indent)
|
||||
onError(offset, 'BAD_INDENT', startColMsg);
|
||||
}
|
||||
if (!keyProps.anchor && !keyProps.tag && !sep) {
|
||||
commentEnd = keyProps.end;
|
||||
if (keyProps.comment) {
|
||||
if (map.comment)
|
||||
map.comment += '\n' + keyProps.comment;
|
||||
else
|
||||
map.comment = keyProps.comment;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (keyProps.newlineAfterProp || utilContainsNewline.containsNewline(key)) {
|
||||
onError(key ?? start[start.length - 1], 'MULTILINE_IMPLICIT_KEY', 'Implicit keys need to be on a single line');
|
||||
}
|
||||
}
|
||||
else if (keyProps.found?.indent !== bm.indent) {
|
||||
onError(offset, 'BAD_INDENT', startColMsg);
|
||||
}
|
||||
// key value
|
||||
ctx.atKey = true;
|
||||
const keyStart = keyProps.end;
|
||||
const keyNode = key
|
||||
? composeNode(ctx, key, keyProps, onError)
|
||||
: composeEmptyNode(ctx, keyStart, start, null, keyProps, onError);
|
||||
if (ctx.schema.compat)
|
||||
utilFlowIndentCheck.flowIndentCheck(bm.indent, key, onError);
|
||||
ctx.atKey = false;
|
||||
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
||||
onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique');
|
||||
// value properties
|
||||
const valueProps = resolveProps.resolveProps(sep ?? [], {
|
||||
indicator: 'map-value-ind',
|
||||
next: value,
|
||||
offset: keyNode.range[2],
|
||||
onError,
|
||||
parentIndent: bm.indent,
|
||||
startOnNewline: !key || key.type === 'block-scalar'
|
||||
});
|
||||
offset = valueProps.end;
|
||||
if (valueProps.found) {
|
||||
if (implicitKey) {
|
||||
if (value?.type === 'block-map' && !valueProps.hasNewline)
|
||||
onError(offset, 'BLOCK_AS_IMPLICIT_KEY', 'Nested mappings are not allowed in compact mappings');
|
||||
if (ctx.options.strict &&
|
||||
keyProps.start < valueProps.found.offset - 1024)
|
||||
onError(keyNode.range, 'KEY_OVER_1024_CHARS', 'The : indicator must be at most 1024 chars after the start of an implicit block mapping key');
|
||||
}
|
||||
// value value
|
||||
const valueNode = value
|
||||
? composeNode(ctx, value, valueProps, onError)
|
||||
: composeEmptyNode(ctx, offset, sep, null, valueProps, onError);
|
||||
if (ctx.schema.compat)
|
||||
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
||||
offset = valueNode.range[2];
|
||||
const pair = new Pair.Pair(keyNode, valueNode);
|
||||
if (ctx.options.keepSourceTokens)
|
||||
pair.srcToken = collItem;
|
||||
map.items.push(pair);
|
||||
}
|
||||
else {
|
||||
// key with no value
|
||||
if (implicitKey)
|
||||
onError(keyNode.range, 'MISSING_CHAR', 'Implicit map keys need to be followed by map values');
|
||||
if (valueProps.comment) {
|
||||
if (keyNode.comment)
|
||||
keyNode.comment += '\n' + valueProps.comment;
|
||||
else
|
||||
keyNode.comment = valueProps.comment;
|
||||
}
|
||||
const pair = new Pair.Pair(keyNode);
|
||||
if (ctx.options.keepSourceTokens)
|
||||
pair.srcToken = collItem;
|
||||
map.items.push(pair);
|
||||
}
|
||||
}
|
||||
if (commentEnd && commentEnd < offset)
|
||||
onError(commentEnd, 'IMPOSSIBLE', 'Map comment with trailing content');
|
||||
map.range = [bm.offset, offset, commentEnd ?? offset];
|
||||
return map;
|
||||
}
|
||||
|
||||
exports.resolveBlockMap = resolveBlockMap;
|
||||
200
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-scalar.js
vendored
Normal file
200
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-scalar.js
vendored
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
|
||||
function resolveBlockScalar(ctx, scalar, onError) {
|
||||
const start = scalar.offset;
|
||||
const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError);
|
||||
if (!header)
|
||||
return { value: '', type: null, comment: '', range: [start, start, start] };
|
||||
const type = header.mode === '>' ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
|
||||
const lines = scalar.source ? splitLines(scalar.source) : [];
|
||||
// determine the end of content & start of chomping
|
||||
let chompStart = lines.length;
|
||||
for (let i = lines.length - 1; i >= 0; --i) {
|
||||
const content = lines[i][1];
|
||||
if (content === '' || content === '\r')
|
||||
chompStart = i;
|
||||
else
|
||||
break;
|
||||
}
|
||||
// shortcut for empty contents
|
||||
if (chompStart === 0) {
|
||||
const value = header.chomp === '+' && lines.length > 0
|
||||
? '\n'.repeat(Math.max(1, lines.length - 1))
|
||||
: '';
|
||||
let end = start + header.length;
|
||||
if (scalar.source)
|
||||
end += scalar.source.length;
|
||||
return { value, type, comment: header.comment, range: [start, end, end] };
|
||||
}
|
||||
// find the indentation level to trim from start
|
||||
let trimIndent = scalar.indent + header.indent;
|
||||
let offset = scalar.offset + header.length;
|
||||
let contentStart = 0;
|
||||
for (let i = 0; i < chompStart; ++i) {
|
||||
const [indent, content] = lines[i];
|
||||
if (content === '' || content === '\r') {
|
||||
if (header.indent === 0 && indent.length > trimIndent)
|
||||
trimIndent = indent.length;
|
||||
}
|
||||
else {
|
||||
if (indent.length < trimIndent) {
|
||||
const message = 'Block scalars with more-indented leading empty lines must use an explicit indentation indicator';
|
||||
onError(offset + indent.length, 'MISSING_CHAR', message);
|
||||
}
|
||||
if (header.indent === 0)
|
||||
trimIndent = indent.length;
|
||||
contentStart = i;
|
||||
if (trimIndent === 0 && !ctx.atRoot) {
|
||||
const message = 'Block scalar values in collections must be indented';
|
||||
onError(offset, 'BAD_INDENT', message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
offset += indent.length + content.length + 1;
|
||||
}
|
||||
// include trailing more-indented empty lines in content
|
||||
for (let i = lines.length - 1; i >= chompStart; --i) {
|
||||
if (lines[i][0].length > trimIndent)
|
||||
chompStart = i + 1;
|
||||
}
|
||||
let value = '';
|
||||
let sep = '';
|
||||
let prevMoreIndented = false;
|
||||
// leading whitespace is kept intact
|
||||
for (let i = 0; i < contentStart; ++i)
|
||||
value += lines[i][0].slice(trimIndent) + '\n';
|
||||
for (let i = contentStart; i < chompStart; ++i) {
|
||||
let [indent, content] = lines[i];
|
||||
offset += indent.length + content.length + 1;
|
||||
const crlf = content[content.length - 1] === '\r';
|
||||
if (crlf)
|
||||
content = content.slice(0, -1);
|
||||
/* istanbul ignore if already caught in lexer */
|
||||
if (content && indent.length < trimIndent) {
|
||||
const src = header.indent
|
||||
? 'explicit indentation indicator'
|
||||
: 'first line';
|
||||
const message = `Block scalar lines must not be less indented than their ${src}`;
|
||||
onError(offset - content.length - (crlf ? 2 : 1), 'BAD_INDENT', message);
|
||||
indent = '';
|
||||
}
|
||||
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
||||
value += sep + indent.slice(trimIndent) + content;
|
||||
sep = '\n';
|
||||
}
|
||||
else if (indent.length > trimIndent || content[0] === '\t') {
|
||||
// more-indented content within a folded block
|
||||
if (sep === ' ')
|
||||
sep = '\n';
|
||||
else if (!prevMoreIndented && sep === '\n')
|
||||
sep = '\n\n';
|
||||
value += sep + indent.slice(trimIndent) + content;
|
||||
sep = '\n';
|
||||
prevMoreIndented = true;
|
||||
}
|
||||
else if (content === '') {
|
||||
// empty line
|
||||
if (sep === '\n')
|
||||
value += '\n';
|
||||
else
|
||||
sep = '\n';
|
||||
}
|
||||
else {
|
||||
value += sep + content;
|
||||
sep = ' ';
|
||||
prevMoreIndented = false;
|
||||
}
|
||||
}
|
||||
switch (header.chomp) {
|
||||
case '-':
|
||||
break;
|
||||
case '+':
|
||||
for (let i = chompStart; i < lines.length; ++i)
|
||||
value += '\n' + lines[i][0].slice(trimIndent);
|
||||
if (value[value.length - 1] !== '\n')
|
||||
value += '\n';
|
||||
break;
|
||||
default:
|
||||
value += '\n';
|
||||
}
|
||||
const end = start + header.length + scalar.source.length;
|
||||
return { value, type, comment: header.comment, range: [start, end, end] };
|
||||
}
|
||||
function parseBlockScalarHeader({ offset, props }, strict, onError) {
|
||||
/* istanbul ignore if should not happen */
|
||||
if (props[0].type !== 'block-scalar-header') {
|
||||
onError(props[0], 'IMPOSSIBLE', 'Block scalar header not found');
|
||||
return null;
|
||||
}
|
||||
const { source } = props[0];
|
||||
const mode = source[0];
|
||||
let indent = 0;
|
||||
let chomp = '';
|
||||
let error = -1;
|
||||
for (let i = 1; i < source.length; ++i) {
|
||||
const ch = source[i];
|
||||
if (!chomp && (ch === '-' || ch === '+'))
|
||||
chomp = ch;
|
||||
else {
|
||||
const n = Number(ch);
|
||||
if (!indent && n)
|
||||
indent = n;
|
||||
else if (error === -1)
|
||||
error = offset + i;
|
||||
}
|
||||
}
|
||||
if (error !== -1)
|
||||
onError(error, 'UNEXPECTED_TOKEN', `Block scalar header includes extra characters: ${source}`);
|
||||
let hasSpace = false;
|
||||
let comment = '';
|
||||
let length = source.length;
|
||||
for (let i = 1; i < props.length; ++i) {
|
||||
const token = props[i];
|
||||
switch (token.type) {
|
||||
case 'space':
|
||||
hasSpace = true;
|
||||
// fallthrough
|
||||
case 'newline':
|
||||
length += token.source.length;
|
||||
break;
|
||||
case 'comment':
|
||||
if (strict && !hasSpace) {
|
||||
const message = 'Comments must be separated from other tokens by white space characters';
|
||||
onError(token, 'MISSING_CHAR', message);
|
||||
}
|
||||
length += token.source.length;
|
||||
comment = token.source.substring(1);
|
||||
break;
|
||||
case 'error':
|
||||
onError(token, 'UNEXPECTED_TOKEN', token.message);
|
||||
length += token.source.length;
|
||||
break;
|
||||
/* istanbul ignore next should not happen */
|
||||
default: {
|
||||
const message = `Unexpected token in block scalar header: ${token.type}`;
|
||||
onError(token, 'UNEXPECTED_TOKEN', message);
|
||||
const ts = token.source;
|
||||
if (ts && typeof ts === 'string')
|
||||
length += ts.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { mode, indent, chomp, comment, length };
|
||||
}
|
||||
/** @returns Array of lines split up as `[indent, content]` */
|
||||
function splitLines(source) {
|
||||
const split = source.split(/\n( *)/);
|
||||
const first = split[0];
|
||||
const m = first.match(/^( *)/);
|
||||
const line0 = m?.[1]
|
||||
? [m[1], first.slice(m[1].length)]
|
||||
: ['', first];
|
||||
const lines = [line0];
|
||||
for (let i = 1; i < split.length; i += 2)
|
||||
lines.push([split[i], split[i + 1]]);
|
||||
return lines;
|
||||
}
|
||||
|
||||
exports.resolveBlockScalar = resolveBlockScalar;
|
||||
51
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-seq.js
vendored
Normal file
51
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-block-seq.js
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
var YAMLSeq = require('../nodes/YAMLSeq.js');
|
||||
var resolveProps = require('./resolve-props.js');
|
||||
var utilFlowIndentCheck = require('./util-flow-indent-check.js');
|
||||
|
||||
function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, tag) {
|
||||
const NodeClass = tag?.nodeClass ?? YAMLSeq.YAMLSeq;
|
||||
const seq = new NodeClass(ctx.schema);
|
||||
if (ctx.atRoot)
|
||||
ctx.atRoot = false;
|
||||
if (ctx.atKey)
|
||||
ctx.atKey = false;
|
||||
let offset = bs.offset;
|
||||
let commentEnd = null;
|
||||
for (const { start, value } of bs.items) {
|
||||
const props = resolveProps.resolveProps(start, {
|
||||
indicator: 'seq-item-ind',
|
||||
next: value,
|
||||
offset,
|
||||
onError,
|
||||
parentIndent: bs.indent,
|
||||
startOnNewline: true
|
||||
});
|
||||
if (!props.found) {
|
||||
if (props.anchor || props.tag || value) {
|
||||
if (value?.type === 'block-seq')
|
||||
onError(props.end, 'BAD_INDENT', 'All sequence items must start at the same column');
|
||||
else
|
||||
onError(offset, 'MISSING_CHAR', 'Sequence item without - indicator');
|
||||
}
|
||||
else {
|
||||
commentEnd = props.end;
|
||||
if (props.comment)
|
||||
seq.comment = props.comment;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const node = value
|
||||
? composeNode(ctx, value, props, onError)
|
||||
: composeEmptyNode(ctx, props.end, start, null, props, onError);
|
||||
if (ctx.schema.compat)
|
||||
utilFlowIndentCheck.flowIndentCheck(bs.indent, value, onError);
|
||||
offset = node.range[2];
|
||||
seq.items.push(node);
|
||||
}
|
||||
seq.range = [bs.offset, offset, commentEnd ?? offset];
|
||||
return seq;
|
||||
}
|
||||
|
||||
exports.resolveBlockSeq = resolveBlockSeq;
|
||||
39
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-end.js
vendored
Normal file
39
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-end.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
function resolveEnd(end, offset, reqSpace, onError) {
|
||||
let comment = '';
|
||||
if (end) {
|
||||
let hasSpace = false;
|
||||
let sep = '';
|
||||
for (const token of end) {
|
||||
const { source, type } = token;
|
||||
switch (type) {
|
||||
case 'space':
|
||||
hasSpace = true;
|
||||
break;
|
||||
case 'comment': {
|
||||
if (reqSpace && !hasSpace)
|
||||
onError(token, 'MISSING_CHAR', 'Comments must be separated from other tokens by white space characters');
|
||||
const cb = source.substring(1) || ' ';
|
||||
if (!comment)
|
||||
comment = cb;
|
||||
else
|
||||
comment += sep + cb;
|
||||
sep = '';
|
||||
break;
|
||||
}
|
||||
case 'newline':
|
||||
if (comment)
|
||||
sep += source;
|
||||
hasSpace = true;
|
||||
break;
|
||||
default:
|
||||
onError(token, 'UNEXPECTED_TOKEN', `Unexpected ${type} at node end`);
|
||||
}
|
||||
offset += source.length;
|
||||
}
|
||||
}
|
||||
return { comment, offset };
|
||||
}
|
||||
|
||||
exports.resolveEnd = resolveEnd;
|
||||
209
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-flow-collection.js
vendored
Normal file
209
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-flow-collection.js
vendored
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Pair = require('../nodes/Pair.js');
|
||||
var YAMLMap = require('../nodes/YAMLMap.js');
|
||||
var YAMLSeq = require('../nodes/YAMLSeq.js');
|
||||
var resolveEnd = require('./resolve-end.js');
|
||||
var resolveProps = require('./resolve-props.js');
|
||||
var utilContainsNewline = require('./util-contains-newline.js');
|
||||
var utilMapIncludes = require('./util-map-includes.js');
|
||||
|
||||
const blockMsg = 'Block collections are not allowed within flow collections';
|
||||
const isBlock = (token) => token && (token.type === 'block-map' || token.type === 'block-seq');
|
||||
function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
|
||||
const isMap = fc.start.source === '{';
|
||||
const fcName = isMap ? 'flow map' : 'flow sequence';
|
||||
const NodeClass = (tag?.nodeClass ?? (isMap ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq));
|
||||
const coll = new NodeClass(ctx.schema);
|
||||
coll.flow = true;
|
||||
const atRoot = ctx.atRoot;
|
||||
if (atRoot)
|
||||
ctx.atRoot = false;
|
||||
if (ctx.atKey)
|
||||
ctx.atKey = false;
|
||||
let offset = fc.offset + fc.start.source.length;
|
||||
for (let i = 0; i < fc.items.length; ++i) {
|
||||
const collItem = fc.items[i];
|
||||
const { start, key, sep, value } = collItem;
|
||||
const props = resolveProps.resolveProps(start, {
|
||||
flow: fcName,
|
||||
indicator: 'explicit-key-ind',
|
||||
next: key ?? sep?.[0],
|
||||
offset,
|
||||
onError,
|
||||
parentIndent: fc.indent,
|
||||
startOnNewline: false
|
||||
});
|
||||
if (!props.found) {
|
||||
if (!props.anchor && !props.tag && !sep && !value) {
|
||||
if (i === 0 && props.comma)
|
||||
onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`);
|
||||
else if (i < fc.items.length - 1)
|
||||
onError(props.start, 'UNEXPECTED_TOKEN', `Unexpected empty item in ${fcName}`);
|
||||
if (props.comment) {
|
||||
if (coll.comment)
|
||||
coll.comment += '\n' + props.comment;
|
||||
else
|
||||
coll.comment = props.comment;
|
||||
}
|
||||
offset = props.end;
|
||||
continue;
|
||||
}
|
||||
if (!isMap && ctx.options.strict && utilContainsNewline.containsNewline(key))
|
||||
onError(key, // checked by containsNewline()
|
||||
'MULTILINE_IMPLICIT_KEY', 'Implicit keys of flow sequence pairs need to be on a single line');
|
||||
}
|
||||
if (i === 0) {
|
||||
if (props.comma)
|
||||
onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`);
|
||||
}
|
||||
else {
|
||||
if (!props.comma)
|
||||
onError(props.start, 'MISSING_CHAR', `Missing , between ${fcName} items`);
|
||||
if (props.comment) {
|
||||
let prevItemComment = '';
|
||||
loop: for (const st of start) {
|
||||
switch (st.type) {
|
||||
case 'comma':
|
||||
case 'space':
|
||||
break;
|
||||
case 'comment':
|
||||
prevItemComment = st.source.substring(1);
|
||||
break loop;
|
||||
default:
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
if (prevItemComment) {
|
||||
let prev = coll.items[coll.items.length - 1];
|
||||
if (identity.isPair(prev))
|
||||
prev = prev.value ?? prev.key;
|
||||
if (prev.comment)
|
||||
prev.comment += '\n' + prevItemComment;
|
||||
else
|
||||
prev.comment = prevItemComment;
|
||||
props.comment = props.comment.substring(prevItemComment.length + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isMap && !sep && !props.found) {
|
||||
// item is a value in a seq
|
||||
// → key & sep are empty, start does not include ? or :
|
||||
const valueNode = value
|
||||
? composeNode(ctx, value, props, onError)
|
||||
: composeEmptyNode(ctx, props.end, sep, null, props, onError);
|
||||
coll.items.push(valueNode);
|
||||
offset = valueNode.range[2];
|
||||
if (isBlock(value))
|
||||
onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg);
|
||||
}
|
||||
else {
|
||||
// item is a key+value pair
|
||||
// key value
|
||||
ctx.atKey = true;
|
||||
const keyStart = props.end;
|
||||
const keyNode = key
|
||||
? composeNode(ctx, key, props, onError)
|
||||
: composeEmptyNode(ctx, keyStart, start, null, props, onError);
|
||||
if (isBlock(key))
|
||||
onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg);
|
||||
ctx.atKey = false;
|
||||
// value properties
|
||||
const valueProps = resolveProps.resolveProps(sep ?? [], {
|
||||
flow: fcName,
|
||||
indicator: 'map-value-ind',
|
||||
next: value,
|
||||
offset: keyNode.range[2],
|
||||
onError,
|
||||
parentIndent: fc.indent,
|
||||
startOnNewline: false
|
||||
});
|
||||
if (valueProps.found) {
|
||||
if (!isMap && !props.found && ctx.options.strict) {
|
||||
if (sep)
|
||||
for (const st of sep) {
|
||||
if (st === valueProps.found)
|
||||
break;
|
||||
if (st.type === 'newline') {
|
||||
onError(st, 'MULTILINE_IMPLICIT_KEY', 'Implicit keys of flow sequence pairs need to be on a single line');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (props.start < valueProps.found.offset - 1024)
|
||||
onError(valueProps.found, 'KEY_OVER_1024_CHARS', 'The : indicator must be at most 1024 chars after the start of an implicit flow sequence key');
|
||||
}
|
||||
}
|
||||
else if (value) {
|
||||
if ('source' in value && value.source?.[0] === ':')
|
||||
onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`);
|
||||
else
|
||||
onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`);
|
||||
}
|
||||
// value value
|
||||
const valueNode = value
|
||||
? composeNode(ctx, value, valueProps, onError)
|
||||
: valueProps.found
|
||||
? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError)
|
||||
: null;
|
||||
if (valueNode) {
|
||||
if (isBlock(value))
|
||||
onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg);
|
||||
}
|
||||
else if (valueProps.comment) {
|
||||
if (keyNode.comment)
|
||||
keyNode.comment += '\n' + valueProps.comment;
|
||||
else
|
||||
keyNode.comment = valueProps.comment;
|
||||
}
|
||||
const pair = new Pair.Pair(keyNode, valueNode);
|
||||
if (ctx.options.keepSourceTokens)
|
||||
pair.srcToken = collItem;
|
||||
if (isMap) {
|
||||
const map = coll;
|
||||
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
||||
onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique');
|
||||
map.items.push(pair);
|
||||
}
|
||||
else {
|
||||
const map = new YAMLMap.YAMLMap(ctx.schema);
|
||||
map.flow = true;
|
||||
map.items.push(pair);
|
||||
const endRange = (valueNode ?? keyNode).range;
|
||||
map.range = [keyNode.range[0], endRange[1], endRange[2]];
|
||||
coll.items.push(map);
|
||||
}
|
||||
offset = valueNode ? valueNode.range[2] : valueProps.end;
|
||||
}
|
||||
}
|
||||
const expectedEnd = isMap ? '}' : ']';
|
||||
const [ce, ...ee] = fc.end;
|
||||
let cePos = offset;
|
||||
if (ce?.source === expectedEnd)
|
||||
cePos = ce.offset + ce.source.length;
|
||||
else {
|
||||
const name = fcName[0].toUpperCase() + fcName.substring(1);
|
||||
const msg = atRoot
|
||||
? `${name} must end with a ${expectedEnd}`
|
||||
: `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`;
|
||||
onError(offset, atRoot ? 'MISSING_CHAR' : 'BAD_INDENT', msg);
|
||||
if (ce && ce.source.length !== 1)
|
||||
ee.unshift(ce);
|
||||
}
|
||||
if (ee.length > 0) {
|
||||
const end = resolveEnd.resolveEnd(ee, cePos, ctx.options.strict, onError);
|
||||
if (end.comment) {
|
||||
if (coll.comment)
|
||||
coll.comment += '\n' + end.comment;
|
||||
else
|
||||
coll.comment = end.comment;
|
||||
}
|
||||
coll.range = [fc.offset, cePos, end.offset];
|
||||
}
|
||||
else {
|
||||
coll.range = [fc.offset, cePos, cePos];
|
||||
}
|
||||
return coll;
|
||||
}
|
||||
|
||||
exports.resolveFlowCollection = resolveFlowCollection;
|
||||
228
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-flow-scalar.js
vendored
Normal file
228
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-flow-scalar.js
vendored
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
var resolveEnd = require('./resolve-end.js');
|
||||
|
||||
function resolveFlowScalar(scalar, strict, onError) {
|
||||
const { offset, type, source, end } = scalar;
|
||||
let _type;
|
||||
let value;
|
||||
const _onError = (rel, code, msg) => onError(offset + rel, code, msg);
|
||||
switch (type) {
|
||||
case 'scalar':
|
||||
_type = Scalar.Scalar.PLAIN;
|
||||
value = plainValue(source, _onError);
|
||||
break;
|
||||
case 'single-quoted-scalar':
|
||||
_type = Scalar.Scalar.QUOTE_SINGLE;
|
||||
value = singleQuotedValue(source, _onError);
|
||||
break;
|
||||
case 'double-quoted-scalar':
|
||||
_type = Scalar.Scalar.QUOTE_DOUBLE;
|
||||
value = doubleQuotedValue(source, _onError);
|
||||
break;
|
||||
/* istanbul ignore next should not happen */
|
||||
default:
|
||||
onError(scalar, 'UNEXPECTED_TOKEN', `Expected a flow scalar value, but found: ${type}`);
|
||||
return {
|
||||
value: '',
|
||||
type: null,
|
||||
comment: '',
|
||||
range: [offset, offset + source.length, offset + source.length]
|
||||
};
|
||||
}
|
||||
const valueEnd = offset + source.length;
|
||||
const re = resolveEnd.resolveEnd(end, valueEnd, strict, onError);
|
||||
return {
|
||||
value,
|
||||
type: _type,
|
||||
comment: re.comment,
|
||||
range: [offset, valueEnd, re.offset]
|
||||
};
|
||||
}
|
||||
function plainValue(source, onError) {
|
||||
let badChar = '';
|
||||
switch (source[0]) {
|
||||
/* istanbul ignore next should not happen */
|
||||
case '\t':
|
||||
badChar = 'a tab character';
|
||||
break;
|
||||
case ',':
|
||||
badChar = 'flow indicator character ,';
|
||||
break;
|
||||
case '%':
|
||||
badChar = 'directive indicator character %';
|
||||
break;
|
||||
case '|':
|
||||
case '>': {
|
||||
badChar = `block scalar indicator ${source[0]}`;
|
||||
break;
|
||||
}
|
||||
case '@':
|
||||
case '`': {
|
||||
badChar = `reserved character ${source[0]}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (badChar)
|
||||
onError(0, 'BAD_SCALAR_START', `Plain value cannot start with ${badChar}`);
|
||||
return unfoldLines(source);
|
||||
}
|
||||
function singleQuotedValue(source, onError) {
|
||||
if (source[source.length - 1] !== "'" || source.length === 1)
|
||||
onError(source.length, 'MISSING_CHAR', "Missing closing 'quote");
|
||||
return unfoldLines(source.slice(1, -1)).replace(/''/g, "'");
|
||||
}
|
||||
function unfoldLines(source) {
|
||||
const line = /(.*?)\r?\n/sy;
|
||||
let match = line.exec(source);
|
||||
if (!match)
|
||||
return source;
|
||||
/**
|
||||
* The negative lookbehinds in these RegExps are to
|
||||
* prevent causing a polynomial search time in certain cases.
|
||||
*
|
||||
* The try-catch is for Safari < 16.4 and other old browsers:
|
||||
* https://caniuse.com/js-regexp-lookbehind
|
||||
*/
|
||||
let trimEnd, trimBoth;
|
||||
try {
|
||||
trimEnd = new RegExp('(?<![ \t])[ \t]+$');
|
||||
trimBoth = new RegExp('^[ \t]+|(?<![ \t])[ \t]+$', 'g');
|
||||
}
|
||||
catch {
|
||||
trimEnd = /[ \t]+$/;
|
||||
trimBoth = /^[ \t]+|[ \t]+$/g;
|
||||
}
|
||||
let res = match[1].replace(trimEnd, '');
|
||||
let sep = ' ';
|
||||
let pos = line.lastIndex;
|
||||
while ((match = line.exec(source))) {
|
||||
const lm = match[1].replace(trimBoth, '');
|
||||
if (lm === '') {
|
||||
if (sep === '\n')
|
||||
res += sep;
|
||||
else
|
||||
sep = '\n';
|
||||
}
|
||||
else {
|
||||
res += sep + lm;
|
||||
sep = ' ';
|
||||
}
|
||||
pos = line.lastIndex;
|
||||
}
|
||||
const last = /[ \t]*(.*)/sy;
|
||||
last.lastIndex = pos;
|
||||
match = last.exec(source);
|
||||
return res + sep + (match?.[1] ?? '');
|
||||
}
|
||||
function doubleQuotedValue(source, onError) {
|
||||
let res = '';
|
||||
for (let i = 1; i < source.length - 1; ++i) {
|
||||
const ch = source[i];
|
||||
if (ch === '\r' && source[i + 1] === '\n')
|
||||
continue;
|
||||
if (ch === '\n') {
|
||||
const { fold, offset } = foldNewline(source, i);
|
||||
res += fold;
|
||||
i = offset;
|
||||
}
|
||||
else if (ch === '\\') {
|
||||
let next = source[++i];
|
||||
const cc = escapeCodes[next];
|
||||
if (cc)
|
||||
res += cc;
|
||||
else if (next === '\n') {
|
||||
// skip escaped newlines, but still trim the following line
|
||||
next = source[i + 1];
|
||||
while (next === ' ' || next === '\t')
|
||||
next = source[++i + 1];
|
||||
}
|
||||
else if (next === '\r' && source[i + 1] === '\n') {
|
||||
// skip escaped CRLF newlines, but still trim the following line
|
||||
next = source[++i + 1];
|
||||
while (next === ' ' || next === '\t')
|
||||
next = source[++i + 1];
|
||||
}
|
||||
else if (next === 'x' || next === 'u' || next === 'U') {
|
||||
const length = next === 'x' ? 2 : next === 'u' ? 4 : 8;
|
||||
res += parseCharCode(source, i + 1, length, onError);
|
||||
i += length;
|
||||
}
|
||||
else {
|
||||
const raw = source.substr(i - 1, 2);
|
||||
onError(i - 1, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`);
|
||||
res += raw;
|
||||
}
|
||||
}
|
||||
else if (ch === ' ' || ch === '\t') {
|
||||
// trim trailing whitespace
|
||||
const wsStart = i;
|
||||
let next = source[i + 1];
|
||||
while (next === ' ' || next === '\t')
|
||||
next = source[++i + 1];
|
||||
if (next !== '\n' && !(next === '\r' && source[i + 2] === '\n'))
|
||||
res += i > wsStart ? source.slice(wsStart, i + 1) : ch;
|
||||
}
|
||||
else {
|
||||
res += ch;
|
||||
}
|
||||
}
|
||||
if (source[source.length - 1] !== '"' || source.length === 1)
|
||||
onError(source.length, 'MISSING_CHAR', 'Missing closing "quote');
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* Fold a single newline into a space, multiple newlines to N - 1 newlines.
|
||||
* Presumes `source[offset] === '\n'`
|
||||
*/
|
||||
function foldNewline(source, offset) {
|
||||
let fold = '';
|
||||
let ch = source[offset + 1];
|
||||
while (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
||||
if (ch === '\r' && source[offset + 2] !== '\n')
|
||||
break;
|
||||
if (ch === '\n')
|
||||
fold += '\n';
|
||||
offset += 1;
|
||||
ch = source[offset + 1];
|
||||
}
|
||||
if (!fold)
|
||||
fold = ' ';
|
||||
return { fold, offset };
|
||||
}
|
||||
const escapeCodes = {
|
||||
'0': '\0', // null character
|
||||
a: '\x07', // bell character
|
||||
b: '\b', // backspace
|
||||
e: '\x1b', // escape character
|
||||
f: '\f', // form feed
|
||||
n: '\n', // line feed
|
||||
r: '\r', // carriage return
|
||||
t: '\t', // horizontal tab
|
||||
v: '\v', // vertical tab
|
||||
N: '\u0085', // Unicode next line
|
||||
_: '\u00a0', // Unicode non-breaking space
|
||||
L: '\u2028', // Unicode line separator
|
||||
P: '\u2029', // Unicode paragraph separator
|
||||
' ': ' ',
|
||||
'"': '"',
|
||||
'/': '/',
|
||||
'\\': '\\',
|
||||
'\t': '\t'
|
||||
};
|
||||
function parseCharCode(source, offset, length, onError) {
|
||||
const cc = source.substr(offset, length);
|
||||
const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
|
||||
const code = ok ? parseInt(cc, 16) : NaN;
|
||||
try {
|
||||
return String.fromCodePoint(code);
|
||||
}
|
||||
catch {
|
||||
const raw = source.substr(offset - 2, length + 2);
|
||||
onError(offset - 2, 'BAD_DQ_ESCAPE', `Invalid escape sequence ${raw}`);
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
exports.resolveFlowScalar = resolveFlowScalar;
|
||||
148
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-props.js
vendored
Normal file
148
bundled-addons/pithos/vendor/yaml/lib/compose/resolve-props.js
vendored
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
'use strict';
|
||||
|
||||
function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) {
|
||||
let spaceBefore = false;
|
||||
let atNewline = startOnNewline;
|
||||
let hasSpace = startOnNewline;
|
||||
let comment = '';
|
||||
let commentSep = '';
|
||||
let hasNewline = false;
|
||||
let reqSpace = false;
|
||||
let tab = null;
|
||||
let anchor = null;
|
||||
let tag = null;
|
||||
let newlineAfterProp = null;
|
||||
let comma = null;
|
||||
let found = null;
|
||||
let start = null;
|
||||
for (const token of tokens) {
|
||||
if (reqSpace) {
|
||||
if (token.type !== 'space' &&
|
||||
token.type !== 'newline' &&
|
||||
token.type !== 'comma')
|
||||
onError(token.offset, 'MISSING_CHAR', 'Tags and anchors must be separated from the next token by white space');
|
||||
reqSpace = false;
|
||||
}
|
||||
if (tab) {
|
||||
if (atNewline && token.type !== 'comment' && token.type !== 'newline') {
|
||||
onError(tab, 'TAB_AS_INDENT', 'Tabs are not allowed as indentation');
|
||||
}
|
||||
tab = null;
|
||||
}
|
||||
switch (token.type) {
|
||||
case 'space':
|
||||
// At the doc level, tabs at line start may be parsed
|
||||
// as leading white space rather than indentation.
|
||||
// In a flow collection, only the parser handles indent.
|
||||
if (!flow &&
|
||||
(indicator !== 'doc-start' || next?.type !== 'flow-collection') &&
|
||||
token.source.includes('\t')) {
|
||||
tab = token;
|
||||
}
|
||||
hasSpace = true;
|
||||
break;
|
||||
case 'comment': {
|
||||
if (!hasSpace)
|
||||
onError(token, 'MISSING_CHAR', 'Comments must be separated from other tokens by white space characters');
|
||||
const cb = token.source.substring(1) || ' ';
|
||||
if (!comment)
|
||||
comment = cb;
|
||||
else
|
||||
comment += commentSep + cb;
|
||||
commentSep = '';
|
||||
atNewline = false;
|
||||
break;
|
||||
}
|
||||
case 'newline':
|
||||
if (atNewline) {
|
||||
if (comment)
|
||||
comment += token.source;
|
||||
else if (!found || indicator !== 'seq-item-ind')
|
||||
spaceBefore = true;
|
||||
}
|
||||
else
|
||||
commentSep += token.source;
|
||||
atNewline = true;
|
||||
hasNewline = true;
|
||||
if (anchor || tag)
|
||||
newlineAfterProp = token;
|
||||
hasSpace = true;
|
||||
break;
|
||||
case 'anchor':
|
||||
if (anchor)
|
||||
onError(token, 'MULTIPLE_ANCHORS', 'A node can have at most one anchor');
|
||||
if (token.source.endsWith(':'))
|
||||
onError(token.offset + token.source.length - 1, 'BAD_ALIAS', 'Anchor ending in : is ambiguous', true);
|
||||
anchor = token;
|
||||
start ?? (start = token.offset);
|
||||
atNewline = false;
|
||||
hasSpace = false;
|
||||
reqSpace = true;
|
||||
break;
|
||||
case 'tag': {
|
||||
if (tag)
|
||||
onError(token, 'MULTIPLE_TAGS', 'A node can have at most one tag');
|
||||
tag = token;
|
||||
start ?? (start = token.offset);
|
||||
atNewline = false;
|
||||
hasSpace = false;
|
||||
reqSpace = true;
|
||||
break;
|
||||
}
|
||||
case indicator:
|
||||
// Could here handle preceding comments differently
|
||||
if (anchor || tag)
|
||||
onError(token, 'BAD_PROP_ORDER', `Anchors and tags must be after the ${token.source} indicator`);
|
||||
if (found)
|
||||
onError(token, 'UNEXPECTED_TOKEN', `Unexpected ${token.source} in ${flow ?? 'collection'}`);
|
||||
found = token;
|
||||
atNewline =
|
||||
indicator === 'seq-item-ind' || indicator === 'explicit-key-ind';
|
||||
hasSpace = false;
|
||||
break;
|
||||
case 'comma':
|
||||
if (flow) {
|
||||
if (comma)
|
||||
onError(token, 'UNEXPECTED_TOKEN', `Unexpected , in ${flow}`);
|
||||
comma = token;
|
||||
atNewline = false;
|
||||
hasSpace = false;
|
||||
break;
|
||||
}
|
||||
// else fallthrough
|
||||
default:
|
||||
onError(token, 'UNEXPECTED_TOKEN', `Unexpected ${token.type} token`);
|
||||
atNewline = false;
|
||||
hasSpace = false;
|
||||
}
|
||||
}
|
||||
const last = tokens[tokens.length - 1];
|
||||
const end = last ? last.offset + last.source.length : offset;
|
||||
if (reqSpace &&
|
||||
next &&
|
||||
next.type !== 'space' &&
|
||||
next.type !== 'newline' &&
|
||||
next.type !== 'comma' &&
|
||||
(next.type !== 'scalar' || next.source !== '')) {
|
||||
onError(next.offset, 'MISSING_CHAR', 'Tags and anchors must be separated from the next token by white space');
|
||||
}
|
||||
if (tab &&
|
||||
((atNewline && tab.indent <= parentIndent) ||
|
||||
next?.type === 'block-map' ||
|
||||
next?.type === 'block-seq'))
|
||||
onError(tab, 'TAB_AS_INDENT', 'Tabs are not allowed as indentation');
|
||||
return {
|
||||
comma,
|
||||
found,
|
||||
spaceBefore,
|
||||
comment,
|
||||
hasNewline,
|
||||
anchor,
|
||||
tag,
|
||||
newlineAfterProp,
|
||||
end,
|
||||
start: start ?? end
|
||||
};
|
||||
}
|
||||
|
||||
exports.resolveProps = resolveProps;
|
||||
36
bundled-addons/pithos/vendor/yaml/lib/compose/util-contains-newline.js
vendored
Normal file
36
bundled-addons/pithos/vendor/yaml/lib/compose/util-contains-newline.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
function containsNewline(key) {
|
||||
if (!key)
|
||||
return null;
|
||||
switch (key.type) {
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'double-quoted-scalar':
|
||||
case 'single-quoted-scalar':
|
||||
if (key.source.includes('\n'))
|
||||
return true;
|
||||
if (key.end)
|
||||
for (const st of key.end)
|
||||
if (st.type === 'newline')
|
||||
return true;
|
||||
return false;
|
||||
case 'flow-collection':
|
||||
for (const it of key.items) {
|
||||
for (const st of it.start)
|
||||
if (st.type === 'newline')
|
||||
return true;
|
||||
if (it.sep)
|
||||
for (const st of it.sep)
|
||||
if (st.type === 'newline')
|
||||
return true;
|
||||
if (containsNewline(it.key) || containsNewline(it.value))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
exports.containsNewline = containsNewline;
|
||||
28
bundled-addons/pithos/vendor/yaml/lib/compose/util-empty-scalar-position.js
vendored
Normal file
28
bundled-addons/pithos/vendor/yaml/lib/compose/util-empty-scalar-position.js
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
function emptyScalarPosition(offset, before, pos) {
|
||||
if (before) {
|
||||
pos ?? (pos = before.length);
|
||||
for (let i = pos - 1; i >= 0; --i) {
|
||||
let st = before[i];
|
||||
switch (st.type) {
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
offset -= st.source.length;
|
||||
continue;
|
||||
}
|
||||
// Technically, an empty scalar is immediately after the last non-empty
|
||||
// node, but it's more useful to place it after any whitespace.
|
||||
st = before[++i];
|
||||
while (st?.type === 'space') {
|
||||
offset += st.source.length;
|
||||
st = before[++i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
exports.emptyScalarPosition = emptyScalarPosition;
|
||||
17
bundled-addons/pithos/vendor/yaml/lib/compose/util-flow-indent-check.js
vendored
Normal file
17
bundled-addons/pithos/vendor/yaml/lib/compose/util-flow-indent-check.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
var utilContainsNewline = require('./util-contains-newline.js');
|
||||
|
||||
function flowIndentCheck(indent, fc, onError) {
|
||||
if (fc?.type === 'flow-collection') {
|
||||
const end = fc.end[0];
|
||||
if (end.indent === indent &&
|
||||
(end.source === ']' || end.source === '}') &&
|
||||
utilContainsNewline.containsNewline(fc)) {
|
||||
const msg = 'Flow end indicator should be more indented than parent';
|
||||
onError(end, 'BAD_INDENT', msg, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.flowIndentCheck = flowIndentCheck;
|
||||
15
bundled-addons/pithos/vendor/yaml/lib/compose/util-map-includes.js
vendored
Normal file
15
bundled-addons/pithos/vendor/yaml/lib/compose/util-map-includes.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
|
||||
function mapIncludes(ctx, items, search) {
|
||||
const { uniqueKeys } = ctx.options;
|
||||
if (uniqueKeys === false)
|
||||
return false;
|
||||
const isEqual = typeof uniqueKeys === 'function'
|
||||
? uniqueKeys
|
||||
: (a, b) => a === b || (identity.isScalar(a) && identity.isScalar(b) && a.value === b.value);
|
||||
return items.some(pair => isEqual(pair.key, search));
|
||||
}
|
||||
|
||||
exports.mapIncludes = mapIncludes;
|
||||
337
bundled-addons/pithos/vendor/yaml/lib/doc/Document.js
vendored
Normal file
337
bundled-addons/pithos/vendor/yaml/lib/doc/Document.js
vendored
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
'use strict';
|
||||
|
||||
var Alias = require('../nodes/Alias.js');
|
||||
var Collection = require('../nodes/Collection.js');
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Pair = require('../nodes/Pair.js');
|
||||
var toJS = require('../nodes/toJS.js');
|
||||
var Schema = require('../schema/Schema.js');
|
||||
var stringifyDocument = require('../stringify/stringifyDocument.js');
|
||||
var anchors = require('./anchors.js');
|
||||
var applyReviver = require('./applyReviver.js');
|
||||
var createNode = require('./createNode.js');
|
||||
var directives = require('./directives.js');
|
||||
|
||||
class Document {
|
||||
constructor(value, replacer, options) {
|
||||
/** A comment before this Document */
|
||||
this.commentBefore = null;
|
||||
/** A comment immediately after this Document */
|
||||
this.comment = null;
|
||||
/** Errors encountered during parsing. */
|
||||
this.errors = [];
|
||||
/** Warnings encountered during parsing. */
|
||||
this.warnings = [];
|
||||
Object.defineProperty(this, identity.NODE_TYPE, { value: identity.DOC });
|
||||
let _replacer = null;
|
||||
if (typeof replacer === 'function' || Array.isArray(replacer)) {
|
||||
_replacer = replacer;
|
||||
}
|
||||
else if (options === undefined && replacer) {
|
||||
options = replacer;
|
||||
replacer = undefined;
|
||||
}
|
||||
const opt = Object.assign({
|
||||
intAsBigInt: false,
|
||||
keepSourceTokens: false,
|
||||
logLevel: 'warn',
|
||||
prettyErrors: true,
|
||||
strict: true,
|
||||
stringKeys: false,
|
||||
uniqueKeys: true,
|
||||
version: '1.2'
|
||||
}, options);
|
||||
this.options = opt;
|
||||
let { version } = opt;
|
||||
if (options?._directives) {
|
||||
this.directives = options._directives.atDocument();
|
||||
if (this.directives.yaml.explicit)
|
||||
version = this.directives.yaml.version;
|
||||
}
|
||||
else
|
||||
this.directives = new directives.Directives({ version });
|
||||
this.setSchema(version, options);
|
||||
// @ts-expect-error We can't really know that this matches Contents.
|
||||
this.contents =
|
||||
value === undefined ? null : this.createNode(value, _replacer, options);
|
||||
}
|
||||
/**
|
||||
* Create a deep copy of this Document and its contents.
|
||||
*
|
||||
* Custom Node values that inherit from `Object` still refer to their original instances.
|
||||
*/
|
||||
clone() {
|
||||
const copy = Object.create(Document.prototype, {
|
||||
[identity.NODE_TYPE]: { value: identity.DOC }
|
||||
});
|
||||
copy.commentBefore = this.commentBefore;
|
||||
copy.comment = this.comment;
|
||||
copy.errors = this.errors.slice();
|
||||
copy.warnings = this.warnings.slice();
|
||||
copy.options = Object.assign({}, this.options);
|
||||
if (this.directives)
|
||||
copy.directives = this.directives.clone();
|
||||
copy.schema = this.schema.clone();
|
||||
// @ts-expect-error We can't really know that this matches Contents.
|
||||
copy.contents = identity.isNode(this.contents)
|
||||
? this.contents.clone(copy.schema)
|
||||
: this.contents;
|
||||
if (this.range)
|
||||
copy.range = this.range.slice();
|
||||
return copy;
|
||||
}
|
||||
/** Adds a value to the document. */
|
||||
add(value) {
|
||||
if (assertCollection(this.contents))
|
||||
this.contents.add(value);
|
||||
}
|
||||
/** Adds a value to the document. */
|
||||
addIn(path, value) {
|
||||
if (assertCollection(this.contents))
|
||||
this.contents.addIn(path, value);
|
||||
}
|
||||
/**
|
||||
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
||||
*
|
||||
* If `node` already has an anchor, `name` is ignored.
|
||||
* Otherwise, the `node.anchor` value will be set to `name`,
|
||||
* or if an anchor with that name is already present in the document,
|
||||
* `name` will be used as a prefix for a new unique anchor.
|
||||
* If `name` is undefined, the generated anchor will use 'a' as a prefix.
|
||||
*/
|
||||
createAlias(node, name) {
|
||||
if (!node.anchor) {
|
||||
const prev = anchors.anchorNames(this);
|
||||
node.anchor =
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
!name || prev.has(name) ? anchors.findNewAnchor(name || 'a', prev) : name;
|
||||
}
|
||||
return new Alias.Alias(node.anchor);
|
||||
}
|
||||
createNode(value, replacer, options) {
|
||||
let _replacer = undefined;
|
||||
if (typeof replacer === 'function') {
|
||||
value = replacer.call({ '': value }, '', value);
|
||||
_replacer = replacer;
|
||||
}
|
||||
else if (Array.isArray(replacer)) {
|
||||
const keyToStr = (v) => typeof v === 'number' || v instanceof String || v instanceof Number;
|
||||
const asStr = replacer.filter(keyToStr).map(String);
|
||||
if (asStr.length > 0)
|
||||
replacer = replacer.concat(asStr);
|
||||
_replacer = replacer;
|
||||
}
|
||||
else if (options === undefined && replacer) {
|
||||
options = replacer;
|
||||
replacer = undefined;
|
||||
}
|
||||
const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {};
|
||||
const { onAnchor, setAnchors, sourceObjects } = anchors.createNodeAnchors(this,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
anchorPrefix || 'a');
|
||||
const ctx = {
|
||||
aliasDuplicateObjects: aliasDuplicateObjects ?? true,
|
||||
keepUndefined: keepUndefined ?? false,
|
||||
onAnchor,
|
||||
onTagObj,
|
||||
replacer: _replacer,
|
||||
schema: this.schema,
|
||||
sourceObjects
|
||||
};
|
||||
const node = createNode.createNode(value, tag, ctx);
|
||||
if (flow && identity.isCollection(node))
|
||||
node.flow = true;
|
||||
setAnchors();
|
||||
return node;
|
||||
}
|
||||
/**
|
||||
* Convert a key and a value into a `Pair` using the current schema,
|
||||
* recursively wrapping all values as `Scalar` or `Collection` nodes.
|
||||
*/
|
||||
createPair(key, value, options = {}) {
|
||||
const k = this.createNode(key, null, options);
|
||||
const v = this.createNode(value, null, options);
|
||||
return new Pair.Pair(k, v);
|
||||
}
|
||||
/**
|
||||
* Removes a value from the document.
|
||||
* @returns `true` if the item was found and removed.
|
||||
*/
|
||||
delete(key) {
|
||||
return assertCollection(this.contents) ? this.contents.delete(key) : false;
|
||||
}
|
||||
/**
|
||||
* Removes a value from the document.
|
||||
* @returns `true` if the item was found and removed.
|
||||
*/
|
||||
deleteIn(path) {
|
||||
if (Collection.isEmptyPath(path)) {
|
||||
if (this.contents == null)
|
||||
return false;
|
||||
// @ts-expect-error Presumed impossible if Strict extends false
|
||||
this.contents = null;
|
||||
return true;
|
||||
}
|
||||
return assertCollection(this.contents)
|
||||
? this.contents.deleteIn(path)
|
||||
: false;
|
||||
}
|
||||
/**
|
||||
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
||||
* scalar values from their surrounding node; to disable set `keepScalar` to
|
||||
* `true` (collections are always returned intact).
|
||||
*/
|
||||
get(key, keepScalar) {
|
||||
return identity.isCollection(this.contents)
|
||||
? this.contents.get(key, keepScalar)
|
||||
: undefined;
|
||||
}
|
||||
/**
|
||||
* Returns item at `path`, or `undefined` if not found. By default unwraps
|
||||
* scalar values from their surrounding node; to disable set `keepScalar` to
|
||||
* `true` (collections are always returned intact).
|
||||
*/
|
||||
getIn(path, keepScalar) {
|
||||
if (Collection.isEmptyPath(path))
|
||||
return !keepScalar && identity.isScalar(this.contents)
|
||||
? this.contents.value
|
||||
: this.contents;
|
||||
return identity.isCollection(this.contents)
|
||||
? this.contents.getIn(path, keepScalar)
|
||||
: undefined;
|
||||
}
|
||||
/**
|
||||
* Checks if the document includes a value with the key `key`.
|
||||
*/
|
||||
has(key) {
|
||||
return identity.isCollection(this.contents) ? this.contents.has(key) : false;
|
||||
}
|
||||
/**
|
||||
* Checks if the document includes a value at `path`.
|
||||
*/
|
||||
hasIn(path) {
|
||||
if (Collection.isEmptyPath(path))
|
||||
return this.contents !== undefined;
|
||||
return identity.isCollection(this.contents) ? this.contents.hasIn(path) : false;
|
||||
}
|
||||
/**
|
||||
* Sets a value in this document. For `!!set`, `value` needs to be a
|
||||
* boolean to add/remove the item from the set.
|
||||
*/
|
||||
set(key, value) {
|
||||
if (this.contents == null) {
|
||||
// @ts-expect-error We can't really know that this matches Contents.
|
||||
this.contents = Collection.collectionFromPath(this.schema, [key], value);
|
||||
}
|
||||
else if (assertCollection(this.contents)) {
|
||||
this.contents.set(key, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets a value in this document. For `!!set`, `value` needs to be a
|
||||
* boolean to add/remove the item from the set.
|
||||
*/
|
||||
setIn(path, value) {
|
||||
if (Collection.isEmptyPath(path)) {
|
||||
// @ts-expect-error We can't really know that this matches Contents.
|
||||
this.contents = value;
|
||||
}
|
||||
else if (this.contents == null) {
|
||||
// @ts-expect-error We can't really know that this matches Contents.
|
||||
this.contents = Collection.collectionFromPath(this.schema, Array.from(path), value);
|
||||
}
|
||||
else if (assertCollection(this.contents)) {
|
||||
this.contents.setIn(path, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Change the YAML version and schema used by the document.
|
||||
* A `null` version disables support for directives, explicit tags, anchors, and aliases.
|
||||
* It also requires the `schema` option to be given as a `Schema` instance value.
|
||||
*
|
||||
* Overrides all previously set schema options.
|
||||
*/
|
||||
setSchema(version, options = {}) {
|
||||
if (typeof version === 'number')
|
||||
version = String(version);
|
||||
let opt;
|
||||
switch (version) {
|
||||
case '1.1':
|
||||
if (this.directives)
|
||||
this.directives.yaml.version = '1.1';
|
||||
else
|
||||
this.directives = new directives.Directives({ version: '1.1' });
|
||||
opt = { resolveKnownTags: false, schema: 'yaml-1.1' };
|
||||
break;
|
||||
case '1.2':
|
||||
case 'next':
|
||||
if (this.directives)
|
||||
this.directives.yaml.version = version;
|
||||
else
|
||||
this.directives = new directives.Directives({ version });
|
||||
opt = { resolveKnownTags: true, schema: 'core' };
|
||||
break;
|
||||
case null:
|
||||
if (this.directives)
|
||||
delete this.directives;
|
||||
opt = null;
|
||||
break;
|
||||
default: {
|
||||
const sv = JSON.stringify(version);
|
||||
throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`);
|
||||
}
|
||||
}
|
||||
// Not using `instanceof Schema` to allow for duck typing
|
||||
if (options.schema instanceof Object)
|
||||
this.schema = options.schema;
|
||||
else if (opt)
|
||||
this.schema = new Schema.Schema(Object.assign(opt, options));
|
||||
else
|
||||
throw new Error(`With a null YAML version, the { schema: Schema } option is required`);
|
||||
}
|
||||
// json & jsonArg are only used from toJSON()
|
||||
toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) {
|
||||
const ctx = {
|
||||
anchors: new Map(),
|
||||
doc: this,
|
||||
keep: !json,
|
||||
mapAsMap: mapAsMap === true,
|
||||
mapKeyWarned: false,
|
||||
maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100
|
||||
};
|
||||
const res = toJS.toJS(this.contents, jsonArg ?? '', ctx);
|
||||
if (typeof onAnchor === 'function')
|
||||
for (const { count, res } of ctx.anchors.values())
|
||||
onAnchor(res, count);
|
||||
return typeof reviver === 'function'
|
||||
? applyReviver.applyReviver(reviver, { '': res }, '', res)
|
||||
: res;
|
||||
}
|
||||
/**
|
||||
* A JSON representation of the document `contents`.
|
||||
*
|
||||
* @param jsonArg Used by `JSON.stringify` to indicate the array index or
|
||||
* property name.
|
||||
*/
|
||||
toJSON(jsonArg, onAnchor) {
|
||||
return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor });
|
||||
}
|
||||
/** A YAML representation of the document. */
|
||||
toString(options = {}) {
|
||||
if (this.errors.length > 0)
|
||||
throw new Error('Document with errors cannot be stringified');
|
||||
if ('indent' in options &&
|
||||
(!Number.isInteger(options.indent) || Number(options.indent) <= 0)) {
|
||||
const s = JSON.stringify(options.indent);
|
||||
throw new Error(`"indent" option must be a positive integer, not ${s}`);
|
||||
}
|
||||
return stringifyDocument.stringifyDocument(this, options);
|
||||
}
|
||||
}
|
||||
function assertCollection(contents) {
|
||||
if (identity.isCollection(contents))
|
||||
return true;
|
||||
throw new Error('Expected a YAML collection as document contents');
|
||||
}
|
||||
|
||||
exports.Document = Document;
|
||||
76
bundled-addons/pithos/vendor/yaml/lib/doc/anchors.js
vendored
Normal file
76
bundled-addons/pithos/vendor/yaml/lib/doc/anchors.js
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var visit = require('../visit.js');
|
||||
|
||||
/**
|
||||
* Verify that the input string is a valid anchor.
|
||||
*
|
||||
* Will throw on errors.
|
||||
*/
|
||||
function anchorIsValid(anchor) {
|
||||
if (/[\x00-\x19\s,[\]{}]/.test(anchor)) {
|
||||
const sa = JSON.stringify(anchor);
|
||||
const msg = `Anchor must not contain whitespace or control characters: ${sa}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function anchorNames(root) {
|
||||
const anchors = new Set();
|
||||
visit.visit(root, {
|
||||
Value(_key, node) {
|
||||
if (node.anchor)
|
||||
anchors.add(node.anchor);
|
||||
}
|
||||
});
|
||||
return anchors;
|
||||
}
|
||||
/** Find a new anchor name with the given `prefix` and a one-indexed suffix. */
|
||||
function findNewAnchor(prefix, exclude) {
|
||||
for (let i = 1; true; ++i) {
|
||||
const name = `${prefix}${i}`;
|
||||
if (!exclude.has(name))
|
||||
return name;
|
||||
}
|
||||
}
|
||||
function createNodeAnchors(doc, prefix) {
|
||||
const aliasObjects = [];
|
||||
const sourceObjects = new Map();
|
||||
let prevAnchors = null;
|
||||
return {
|
||||
onAnchor: (source) => {
|
||||
aliasObjects.push(source);
|
||||
prevAnchors ?? (prevAnchors = anchorNames(doc));
|
||||
const anchor = findNewAnchor(prefix, prevAnchors);
|
||||
prevAnchors.add(anchor);
|
||||
return anchor;
|
||||
},
|
||||
/**
|
||||
* With circular references, the source node is only resolved after all
|
||||
* of its child nodes are. This is why anchors are set only after all of
|
||||
* the nodes have been created.
|
||||
*/
|
||||
setAnchors: () => {
|
||||
for (const source of aliasObjects) {
|
||||
const ref = sourceObjects.get(source);
|
||||
if (typeof ref === 'object' &&
|
||||
ref.anchor &&
|
||||
(identity.isScalar(ref.node) || identity.isCollection(ref.node))) {
|
||||
ref.node.anchor = ref.anchor;
|
||||
}
|
||||
else {
|
||||
const error = new Error('Failed to resolve repeated object (this should not happen)');
|
||||
error.source = source;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
sourceObjects
|
||||
};
|
||||
}
|
||||
|
||||
exports.anchorIsValid = anchorIsValid;
|
||||
exports.anchorNames = anchorNames;
|
||||
exports.createNodeAnchors = createNodeAnchors;
|
||||
exports.findNewAnchor = findNewAnchor;
|
||||
57
bundled-addons/pithos/vendor/yaml/lib/doc/applyReviver.js
vendored
Normal file
57
bundled-addons/pithos/vendor/yaml/lib/doc/applyReviver.js
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Applies the JSON.parse reviver algorithm as defined in the ECMA-262 spec,
|
||||
* in section 24.5.1.1 "Runtime Semantics: InternalizeJSONProperty" of the
|
||||
* 2021 edition: https://tc39.es/ecma262/#sec-json.parse
|
||||
*
|
||||
* Includes extensions for handling Map and Set objects.
|
||||
*/
|
||||
function applyReviver(reviver, obj, key, val) {
|
||||
if (val && typeof val === 'object') {
|
||||
if (Array.isArray(val)) {
|
||||
for (let i = 0, len = val.length; i < len; ++i) {
|
||||
const v0 = val[i];
|
||||
const v1 = applyReviver(reviver, val, String(i), v0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-array-delete
|
||||
if (v1 === undefined)
|
||||
delete val[i];
|
||||
else if (v1 !== v0)
|
||||
val[i] = v1;
|
||||
}
|
||||
}
|
||||
else if (val instanceof Map) {
|
||||
for (const k of Array.from(val.keys())) {
|
||||
const v0 = val.get(k);
|
||||
const v1 = applyReviver(reviver, val, k, v0);
|
||||
if (v1 === undefined)
|
||||
val.delete(k);
|
||||
else if (v1 !== v0)
|
||||
val.set(k, v1);
|
||||
}
|
||||
}
|
||||
else if (val instanceof Set) {
|
||||
for (const v0 of Array.from(val)) {
|
||||
const v1 = applyReviver(reviver, val, v0, v0);
|
||||
if (v1 === undefined)
|
||||
val.delete(v0);
|
||||
else if (v1 !== v0) {
|
||||
val.delete(v0);
|
||||
val.add(v1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const [k, v0] of Object.entries(val)) {
|
||||
const v1 = applyReviver(reviver, val, k, v0);
|
||||
if (v1 === undefined)
|
||||
delete val[k];
|
||||
else if (v1 !== v0)
|
||||
val[k] = v1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(obj, key, val);
|
||||
}
|
||||
|
||||
exports.applyReviver = applyReviver;
|
||||
90
bundled-addons/pithos/vendor/yaml/lib/doc/createNode.js
vendored
Normal file
90
bundled-addons/pithos/vendor/yaml/lib/doc/createNode.js
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
'use strict';
|
||||
|
||||
var Alias = require('../nodes/Alias.js');
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
|
||||
const defaultTagPrefix = 'tag:yaml.org,2002:';
|
||||
function findTagObject(value, tagName, tags) {
|
||||
if (tagName) {
|
||||
const match = tags.filter(t => t.tag === tagName);
|
||||
const tagObj = match.find(t => !t.format) ?? match[0];
|
||||
if (!tagObj)
|
||||
throw new Error(`Tag ${tagName} not found`);
|
||||
return tagObj;
|
||||
}
|
||||
return tags.find(t => t.identify?.(value) && !t.format);
|
||||
}
|
||||
function createNode(value, tagName, ctx) {
|
||||
if (identity.isDocument(value))
|
||||
value = value.contents;
|
||||
if (identity.isNode(value))
|
||||
return value;
|
||||
if (identity.isPair(value)) {
|
||||
const map = ctx.schema[identity.MAP].createNode?.(ctx.schema, null, ctx);
|
||||
map.items.push(value);
|
||||
return map;
|
||||
}
|
||||
if (value instanceof String ||
|
||||
value instanceof Number ||
|
||||
value instanceof Boolean ||
|
||||
(typeof BigInt !== 'undefined' && value instanceof BigInt) // not supported everywhere
|
||||
) {
|
||||
// https://tc39.es/ecma262/#sec-serializejsonproperty
|
||||
value = value.valueOf();
|
||||
}
|
||||
const { aliasDuplicateObjects, onAnchor, onTagObj, schema, sourceObjects } = ctx;
|
||||
// Detect duplicate references to the same object & use Alias nodes for all
|
||||
// after first. The `ref` wrapper allows for circular references to resolve.
|
||||
let ref = undefined;
|
||||
if (aliasDuplicateObjects && value && typeof value === 'object') {
|
||||
ref = sourceObjects.get(value);
|
||||
if (ref) {
|
||||
ref.anchor ?? (ref.anchor = onAnchor(value));
|
||||
return new Alias.Alias(ref.anchor);
|
||||
}
|
||||
else {
|
||||
ref = { anchor: null, node: null };
|
||||
sourceObjects.set(value, ref);
|
||||
}
|
||||
}
|
||||
if (tagName?.startsWith('!!'))
|
||||
tagName = defaultTagPrefix + tagName.slice(2);
|
||||
let tagObj = findTagObject(value, tagName, schema.tags);
|
||||
if (!tagObj) {
|
||||
if (value && typeof value.toJSON === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
value = value.toJSON();
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
const node = new Scalar.Scalar(value);
|
||||
if (ref)
|
||||
ref.node = node;
|
||||
return node;
|
||||
}
|
||||
tagObj =
|
||||
value instanceof Map
|
||||
? schema[identity.MAP]
|
||||
: Symbol.iterator in Object(value)
|
||||
? schema[identity.SEQ]
|
||||
: schema[identity.MAP];
|
||||
}
|
||||
if (onTagObj) {
|
||||
onTagObj(tagObj);
|
||||
delete ctx.onTagObj;
|
||||
}
|
||||
const node = tagObj?.createNode
|
||||
? tagObj.createNode(ctx.schema, value, ctx)
|
||||
: typeof tagObj?.nodeClass?.from === 'function'
|
||||
? tagObj.nodeClass.from(ctx.schema, value, ctx)
|
||||
: new Scalar.Scalar(value);
|
||||
if (tagName)
|
||||
node.tag = tagName;
|
||||
else if (!tagObj.default)
|
||||
node.tag = tagObj.tag;
|
||||
if (ref)
|
||||
ref.node = node;
|
||||
return node;
|
||||
}
|
||||
|
||||
exports.createNode = createNode;
|
||||
178
bundled-addons/pithos/vendor/yaml/lib/doc/directives.js
vendored
Normal file
178
bundled-addons/pithos/vendor/yaml/lib/doc/directives.js
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var visit = require('../visit.js');
|
||||
|
||||
const escapeChars = {
|
||||
'!': '%21',
|
||||
',': '%2C',
|
||||
'[': '%5B',
|
||||
']': '%5D',
|
||||
'{': '%7B',
|
||||
'}': '%7D'
|
||||
};
|
||||
const escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
|
||||
class Directives {
|
||||
constructor(yaml, tags) {
|
||||
/**
|
||||
* The directives-end/doc-start marker `---`. If `null`, a marker may still be
|
||||
* included in the document's stringified representation.
|
||||
*/
|
||||
this.docStart = null;
|
||||
/** The doc-end marker `...`. */
|
||||
this.docEnd = false;
|
||||
this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
|
||||
this.tags = Object.assign({}, Directives.defaultTags, tags);
|
||||
}
|
||||
clone() {
|
||||
const copy = new Directives(this.yaml, this.tags);
|
||||
copy.docStart = this.docStart;
|
||||
return copy;
|
||||
}
|
||||
/**
|
||||
* During parsing, get a Directives instance for the current document and
|
||||
* update the stream state according to the current version's spec.
|
||||
*/
|
||||
atDocument() {
|
||||
const res = new Directives(this.yaml, this.tags);
|
||||
switch (this.yaml.version) {
|
||||
case '1.1':
|
||||
this.atNextDocument = true;
|
||||
break;
|
||||
case '1.2':
|
||||
this.atNextDocument = false;
|
||||
this.yaml = {
|
||||
explicit: Directives.defaultYaml.explicit,
|
||||
version: '1.2'
|
||||
};
|
||||
this.tags = Object.assign({}, Directives.defaultTags);
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @param onError - May be called even if the action was successful
|
||||
* @returns `true` on success
|
||||
*/
|
||||
add(line, onError) {
|
||||
if (this.atNextDocument) {
|
||||
this.yaml = { explicit: Directives.defaultYaml.explicit, version: '1.1' };
|
||||
this.tags = Object.assign({}, Directives.defaultTags);
|
||||
this.atNextDocument = false;
|
||||
}
|
||||
const parts = line.trim().split(/[ \t]+/);
|
||||
const name = parts.shift();
|
||||
switch (name) {
|
||||
case '%TAG': {
|
||||
if (parts.length !== 2) {
|
||||
onError(0, '%TAG directive should contain exactly two parts');
|
||||
if (parts.length < 2)
|
||||
return false;
|
||||
}
|
||||
const [handle, prefix] = parts;
|
||||
this.tags[handle] = prefix;
|
||||
return true;
|
||||
}
|
||||
case '%YAML': {
|
||||
this.yaml.explicit = true;
|
||||
if (parts.length !== 1) {
|
||||
onError(0, '%YAML directive should contain exactly one part');
|
||||
return false;
|
||||
}
|
||||
const [version] = parts;
|
||||
if (version === '1.1' || version === '1.2') {
|
||||
this.yaml.version = version;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const isValid = /^\d+\.\d+$/.test(version);
|
||||
onError(6, `Unsupported YAML version ${version}`, isValid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
onError(0, `Unknown directive ${name}`, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolves a tag, matching handles to those defined in %TAG directives.
|
||||
*
|
||||
* @returns Resolved tag, which may also be the non-specific tag `'!'` or a
|
||||
* `'!local'` tag, or `null` if unresolvable.
|
||||
*/
|
||||
tagName(source, onError) {
|
||||
if (source === '!')
|
||||
return '!'; // non-specific tag
|
||||
if (source[0] !== '!') {
|
||||
onError(`Not a valid tag: ${source}`);
|
||||
return null;
|
||||
}
|
||||
if (source[1] === '<') {
|
||||
const verbatim = source.slice(2, -1);
|
||||
if (verbatim === '!' || verbatim === '!!') {
|
||||
onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
|
||||
return null;
|
||||
}
|
||||
if (source[source.length - 1] !== '>')
|
||||
onError('Verbatim tags must end with a >');
|
||||
return verbatim;
|
||||
}
|
||||
const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
|
||||
if (!suffix)
|
||||
onError(`The ${source} tag has no suffix`);
|
||||
const prefix = this.tags[handle];
|
||||
if (prefix) {
|
||||
try {
|
||||
return prefix + decodeURIComponent(suffix);
|
||||
}
|
||||
catch (error) {
|
||||
onError(String(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (handle === '!')
|
||||
return source; // local tag
|
||||
onError(`Could not resolve tag: ${source}`);
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Given a fully resolved tag, returns its printable string form,
|
||||
* taking into account current tag prefixes and defaults.
|
||||
*/
|
||||
tagString(tag) {
|
||||
for (const [handle, prefix] of Object.entries(this.tags)) {
|
||||
if (tag.startsWith(prefix))
|
||||
return handle + escapeTagName(tag.substring(prefix.length));
|
||||
}
|
||||
return tag[0] === '!' ? tag : `!<${tag}>`;
|
||||
}
|
||||
toString(doc) {
|
||||
const lines = this.yaml.explicit
|
||||
? [`%YAML ${this.yaml.version || '1.2'}`]
|
||||
: [];
|
||||
const tagEntries = Object.entries(this.tags);
|
||||
let tagNames;
|
||||
if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
|
||||
const tags = {};
|
||||
visit.visit(doc.contents, (_key, node) => {
|
||||
if (identity.isNode(node) && node.tag)
|
||||
tags[node.tag] = true;
|
||||
});
|
||||
tagNames = Object.keys(tags);
|
||||
}
|
||||
else
|
||||
tagNames = [];
|
||||
for (const [handle, prefix] of tagEntries) {
|
||||
if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
|
||||
continue;
|
||||
if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
|
||||
lines.push(`%TAG ${handle} ${prefix}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
Directives.defaultYaml = { explicit: false, version: '1.2' };
|
||||
Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
|
||||
|
||||
exports.Directives = Directives;
|
||||
62
bundled-addons/pithos/vendor/yaml/lib/errors.js
vendored
Normal file
62
bundled-addons/pithos/vendor/yaml/lib/errors.js
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
class YAMLError extends Error {
|
||||
constructor(name, pos, code, message) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.pos = pos;
|
||||
}
|
||||
}
|
||||
class YAMLParseError extends YAMLError {
|
||||
constructor(pos, code, message) {
|
||||
super('YAMLParseError', pos, code, message);
|
||||
}
|
||||
}
|
||||
class YAMLWarning extends YAMLError {
|
||||
constructor(pos, code, message) {
|
||||
super('YAMLWarning', pos, code, message);
|
||||
}
|
||||
}
|
||||
const prettifyError = (src, lc) => (error) => {
|
||||
if (error.pos[0] === -1)
|
||||
return;
|
||||
error.linePos = error.pos.map(pos => lc.linePos(pos));
|
||||
const { line, col } = error.linePos[0];
|
||||
error.message += ` at line ${line}, column ${col}`;
|
||||
let ci = col - 1;
|
||||
let lineStr = src
|
||||
.substring(lc.lineStarts[line - 1], lc.lineStarts[line])
|
||||
.replace(/[\n\r]+$/, '');
|
||||
// Trim to max 80 chars, keeping col position near the middle
|
||||
if (ci >= 60 && lineStr.length > 80) {
|
||||
const trimStart = Math.min(ci - 39, lineStr.length - 79);
|
||||
lineStr = '…' + lineStr.substring(trimStart);
|
||||
ci -= trimStart - 1;
|
||||
}
|
||||
if (lineStr.length > 80)
|
||||
lineStr = lineStr.substring(0, 79) + '…';
|
||||
// Include previous line in context if pointing at line start
|
||||
if (line > 1 && /^ *$/.test(lineStr.substring(0, ci))) {
|
||||
// Regexp won't match if start is trimmed
|
||||
let prev = src.substring(lc.lineStarts[line - 2], lc.lineStarts[line - 1]);
|
||||
if (prev.length > 80)
|
||||
prev = prev.substring(0, 79) + '…\n';
|
||||
lineStr = prev + lineStr;
|
||||
}
|
||||
if (/[^ ]/.test(lineStr)) {
|
||||
let count = 1;
|
||||
const end = error.linePos[1];
|
||||
if (end?.line === line && end.col > col) {
|
||||
count = Math.max(1, Math.min(end.col - col, 80 - ci));
|
||||
}
|
||||
const pointer = ' '.repeat(ci) + '^'.repeat(count);
|
||||
error.message += `:\n\n${lineStr}\n${pointer}\n`;
|
||||
}
|
||||
};
|
||||
|
||||
exports.YAMLError = YAMLError;
|
||||
exports.YAMLParseError = YAMLParseError;
|
||||
exports.YAMLWarning = YAMLWarning;
|
||||
exports.prettifyError = prettifyError;
|
||||
50
bundled-addons/pithos/vendor/yaml/lib/index.js
vendored
Normal file
50
bundled-addons/pithos/vendor/yaml/lib/index.js
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
var composer = require('./compose/composer.js');
|
||||
var Document = require('./doc/Document.js');
|
||||
var Schema = require('./schema/Schema.js');
|
||||
var errors = require('./errors.js');
|
||||
var Alias = require('./nodes/Alias.js');
|
||||
var identity = require('./nodes/identity.js');
|
||||
var Pair = require('./nodes/Pair.js');
|
||||
var Scalar = require('./nodes/Scalar.js');
|
||||
var YAMLMap = require('./nodes/YAMLMap.js');
|
||||
var YAMLSeq = require('./nodes/YAMLSeq.js');
|
||||
var cst = require('./parse/cst.js');
|
||||
var lexer = require('./parse/lexer.js');
|
||||
var lineCounter = require('./parse/line-counter.js');
|
||||
var parser = require('./parse/parser.js');
|
||||
var publicApi = require('./public-api.js');
|
||||
var visit = require('./visit.js');
|
||||
|
||||
|
||||
|
||||
exports.Composer = composer.Composer;
|
||||
exports.Document = Document.Document;
|
||||
exports.Schema = Schema.Schema;
|
||||
exports.YAMLError = errors.YAMLError;
|
||||
exports.YAMLParseError = errors.YAMLParseError;
|
||||
exports.YAMLWarning = errors.YAMLWarning;
|
||||
exports.Alias = Alias.Alias;
|
||||
exports.isAlias = identity.isAlias;
|
||||
exports.isCollection = identity.isCollection;
|
||||
exports.isDocument = identity.isDocument;
|
||||
exports.isMap = identity.isMap;
|
||||
exports.isNode = identity.isNode;
|
||||
exports.isPair = identity.isPair;
|
||||
exports.isScalar = identity.isScalar;
|
||||
exports.isSeq = identity.isSeq;
|
||||
exports.Pair = Pair.Pair;
|
||||
exports.Scalar = Scalar.Scalar;
|
||||
exports.YAMLMap = YAMLMap.YAMLMap;
|
||||
exports.YAMLSeq = YAMLSeq.YAMLSeq;
|
||||
exports.CST = cst;
|
||||
exports.Lexer = lexer.Lexer;
|
||||
exports.LineCounter = lineCounter.LineCounter;
|
||||
exports.Parser = parser.Parser;
|
||||
exports.parse = publicApi.parse;
|
||||
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
||||
exports.parseDocument = publicApi.parseDocument;
|
||||
exports.stringify = publicApi.stringify;
|
||||
exports.visit = visit.visit;
|
||||
exports.visitAsync = visit.visitAsync;
|
||||
19
bundled-addons/pithos/vendor/yaml/lib/log.js
vendored
Normal file
19
bundled-addons/pithos/vendor/yaml/lib/log.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
var node_process = require('process');
|
||||
|
||||
function debug(logLevel, ...messages) {
|
||||
if (logLevel === 'debug')
|
||||
console.log(...messages);
|
||||
}
|
||||
function warn(logLevel, warning) {
|
||||
if (logLevel === 'debug' || logLevel === 'warn') {
|
||||
if (typeof node_process.emitWarning === 'function')
|
||||
node_process.emitWarning(warning);
|
||||
else
|
||||
console.warn(warning);
|
||||
}
|
||||
}
|
||||
|
||||
exports.debug = debug;
|
||||
exports.warn = warn;
|
||||
120
bundled-addons/pithos/vendor/yaml/lib/nodes/Alias.js
vendored
Normal file
120
bundled-addons/pithos/vendor/yaml/lib/nodes/Alias.js
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
'use strict';
|
||||
|
||||
var anchors = require('../doc/anchors.js');
|
||||
var visit = require('../visit.js');
|
||||
var identity = require('./identity.js');
|
||||
var Node = require('./Node.js');
|
||||
var toJS = require('./toJS.js');
|
||||
|
||||
class Alias extends Node.NodeBase {
|
||||
constructor(source) {
|
||||
super(identity.ALIAS);
|
||||
this.source = source;
|
||||
Object.defineProperty(this, 'tag', {
|
||||
set() {
|
||||
throw new Error('Alias nodes cannot have tags');
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Resolve the value of this alias within `doc`, finding the last
|
||||
* instance of the `source` anchor before this node.
|
||||
*/
|
||||
resolve(doc, ctx) {
|
||||
if (ctx?.maxAliasCount === 0)
|
||||
throw new ReferenceError('Alias resolution is disabled');
|
||||
let nodes;
|
||||
if (ctx?.aliasResolveCache) {
|
||||
nodes = ctx.aliasResolveCache;
|
||||
}
|
||||
else {
|
||||
nodes = [];
|
||||
visit.visit(doc, {
|
||||
Node: (_key, node) => {
|
||||
if (identity.isAlias(node) || identity.hasAnchor(node))
|
||||
nodes.push(node);
|
||||
}
|
||||
});
|
||||
if (ctx)
|
||||
ctx.aliasResolveCache = nodes;
|
||||
}
|
||||
let found = undefined;
|
||||
for (const node of nodes) {
|
||||
if (node === this)
|
||||
break;
|
||||
if (node.anchor === this.source)
|
||||
found = node;
|
||||
}
|
||||
if (found && ctx) {
|
||||
const { anchors, doc, maxAliasCount } = ctx;
|
||||
let data = anchors.get(found);
|
||||
if (!data) {
|
||||
// Resolve anchors for Node.prototype.toJS()
|
||||
toJS.toJS(found, null, ctx);
|
||||
data = anchors.get(found);
|
||||
}
|
||||
/* istanbul ignore if */
|
||||
if (data?.res === undefined) {
|
||||
const msg = 'This should not happen: Alias anchor was not resolved?';
|
||||
throw new ReferenceError(msg);
|
||||
}
|
||||
if (maxAliasCount >= 0) {
|
||||
data.count += 1;
|
||||
if (data.aliasCount === 0)
|
||||
data.aliasCount = getAliasCount(doc, found, anchors);
|
||||
if (data.count * data.aliasCount > maxAliasCount) {
|
||||
const msg = 'Excessive alias count indicates a resource exhaustion attack';
|
||||
throw new ReferenceError(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
toJSON(_arg, ctx) {
|
||||
if (!ctx)
|
||||
return { source: this.source };
|
||||
const source = this.resolve(ctx.doc, ctx);
|
||||
if (!source) {
|
||||
const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`;
|
||||
throw new ReferenceError(msg);
|
||||
}
|
||||
return ctx.anchors.get(source).res;
|
||||
}
|
||||
toString(ctx, _onComment, _onChompKeep) {
|
||||
const src = `*${this.source}`;
|
||||
if (ctx) {
|
||||
anchors.anchorIsValid(this.source);
|
||||
if (ctx.options.verifyAliasOrder && !ctx.anchors.has(this.source)) {
|
||||
const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
if (ctx.implicitKey)
|
||||
return `${src} `;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
}
|
||||
function getAliasCount(doc, node, anchors) {
|
||||
if (identity.isAlias(node)) {
|
||||
const source = node.resolve(doc);
|
||||
const anchor = anchors && source && anchors.get(source);
|
||||
return anchor ? anchor.count * anchor.aliasCount : 0;
|
||||
}
|
||||
else if (identity.isCollection(node)) {
|
||||
let count = 0;
|
||||
for (const item of node.items) {
|
||||
const c = getAliasCount(doc, item, anchors);
|
||||
if (c > count)
|
||||
count = c;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
else if (identity.isPair(node)) {
|
||||
const kc = getAliasCount(doc, node.key, anchors);
|
||||
const vc = getAliasCount(doc, node.value, anchors);
|
||||
return Math.max(kc, vc);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
exports.Alias = Alias;
|
||||
151
bundled-addons/pithos/vendor/yaml/lib/nodes/Collection.js
vendored
Normal file
151
bundled-addons/pithos/vendor/yaml/lib/nodes/Collection.js
vendored
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
'use strict';
|
||||
|
||||
var createNode = require('../doc/createNode.js');
|
||||
var identity = require('./identity.js');
|
||||
var Node = require('./Node.js');
|
||||
|
||||
function collectionFromPath(schema, path, value) {
|
||||
let v = value;
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
const k = path[i];
|
||||
if (typeof k === 'number' && Number.isInteger(k) && k >= 0) {
|
||||
const a = [];
|
||||
a[k] = v;
|
||||
v = a;
|
||||
}
|
||||
else {
|
||||
v = new Map([[k, v]]);
|
||||
}
|
||||
}
|
||||
return createNode.createNode(v, undefined, {
|
||||
aliasDuplicateObjects: false,
|
||||
keepUndefined: false,
|
||||
onAnchor: () => {
|
||||
throw new Error('This should not happen, please report a bug.');
|
||||
},
|
||||
schema,
|
||||
sourceObjects: new Map()
|
||||
});
|
||||
}
|
||||
// Type guard is intentionally a little wrong so as to be more useful,
|
||||
// as it does not cover untypable empty non-string iterables (e.g. []).
|
||||
const isEmptyPath = (path) => path == null ||
|
||||
(typeof path === 'object' && !!path[Symbol.iterator]().next().done);
|
||||
class Collection extends Node.NodeBase {
|
||||
constructor(type, schema) {
|
||||
super(type);
|
||||
Object.defineProperty(this, 'schema', {
|
||||
value: schema,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a copy of this collection.
|
||||
*
|
||||
* @param schema - If defined, overwrites the original's schema
|
||||
*/
|
||||
clone(schema) {
|
||||
const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
|
||||
if (schema)
|
||||
copy.schema = schema;
|
||||
copy.items = copy.items.map(it => identity.isNode(it) || identity.isPair(it) ? it.clone(schema) : it);
|
||||
if (this.range)
|
||||
copy.range = this.range.slice();
|
||||
return copy;
|
||||
}
|
||||
/**
|
||||
* Adds a value to the collection. For `!!map` and `!!omap` the value must
|
||||
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
||||
* that already exists in the map.
|
||||
*/
|
||||
addIn(path, value) {
|
||||
if (isEmptyPath(path))
|
||||
this.add(value);
|
||||
else {
|
||||
const [key, ...rest] = path;
|
||||
const node = this.get(key, true);
|
||||
if (identity.isCollection(node))
|
||||
node.addIn(rest, value);
|
||||
else if (node === undefined && this.schema)
|
||||
this.set(key, collectionFromPath(this.schema, rest, value));
|
||||
else
|
||||
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes a value from the collection.
|
||||
* @returns `true` if the item was found and removed.
|
||||
*/
|
||||
deleteIn(path) {
|
||||
const [key, ...rest] = path;
|
||||
if (rest.length === 0)
|
||||
return this.delete(key);
|
||||
const node = this.get(key, true);
|
||||
if (identity.isCollection(node))
|
||||
return node.deleteIn(rest);
|
||||
else
|
||||
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
||||
}
|
||||
/**
|
||||
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
||||
* scalar values from their surrounding node; to disable set `keepScalar` to
|
||||
* `true` (collections are always returned intact).
|
||||
*/
|
||||
getIn(path, keepScalar) {
|
||||
const [key, ...rest] = path;
|
||||
const node = this.get(key, true);
|
||||
if (rest.length === 0)
|
||||
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
||||
else
|
||||
return identity.isCollection(node) ? node.getIn(rest, keepScalar) : undefined;
|
||||
}
|
||||
hasAllNullValues(allowScalar) {
|
||||
return this.items.every(node => {
|
||||
if (!identity.isPair(node))
|
||||
return false;
|
||||
const n = node.value;
|
||||
return (n == null ||
|
||||
(allowScalar &&
|
||||
identity.isScalar(n) &&
|
||||
n.value == null &&
|
||||
!n.commentBefore &&
|
||||
!n.comment &&
|
||||
!n.tag));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Checks if the collection includes a value with the key `key`.
|
||||
*/
|
||||
hasIn(path) {
|
||||
const [key, ...rest] = path;
|
||||
if (rest.length === 0)
|
||||
return this.has(key);
|
||||
const node = this.get(key, true);
|
||||
return identity.isCollection(node) ? node.hasIn(rest) : false;
|
||||
}
|
||||
/**
|
||||
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
||||
* boolean to add/remove the item from the set.
|
||||
*/
|
||||
setIn(path, value) {
|
||||
const [key, ...rest] = path;
|
||||
if (rest.length === 0) {
|
||||
this.set(key, value);
|
||||
}
|
||||
else {
|
||||
const node = this.get(key, true);
|
||||
if (identity.isCollection(node))
|
||||
node.setIn(rest, value);
|
||||
else if (node === undefined && this.schema)
|
||||
this.set(key, collectionFromPath(this.schema, rest, value));
|
||||
else
|
||||
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Collection = Collection;
|
||||
exports.collectionFromPath = collectionFromPath;
|
||||
exports.isEmptyPath = isEmptyPath;
|
||||
40
bundled-addons/pithos/vendor/yaml/lib/nodes/Node.js
vendored
Normal file
40
bundled-addons/pithos/vendor/yaml/lib/nodes/Node.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
'use strict';
|
||||
|
||||
var applyReviver = require('../doc/applyReviver.js');
|
||||
var identity = require('./identity.js');
|
||||
var toJS = require('./toJS.js');
|
||||
|
||||
class NodeBase {
|
||||
constructor(type) {
|
||||
Object.defineProperty(this, identity.NODE_TYPE, { value: type });
|
||||
}
|
||||
/** Create a copy of this node. */
|
||||
clone() {
|
||||
const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
|
||||
if (this.range)
|
||||
copy.range = this.range.slice();
|
||||
return copy;
|
||||
}
|
||||
/** A plain JavaScript representation of this node. */
|
||||
toJS(doc, { mapAsMap, maxAliasCount, onAnchor, reviver } = {}) {
|
||||
if (!identity.isDocument(doc))
|
||||
throw new TypeError('A document argument is required');
|
||||
const ctx = {
|
||||
anchors: new Map(),
|
||||
doc,
|
||||
keep: true,
|
||||
mapAsMap: mapAsMap === true,
|
||||
mapKeyWarned: false,
|
||||
maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100
|
||||
};
|
||||
const res = toJS.toJS(this, '', ctx);
|
||||
if (typeof onAnchor === 'function')
|
||||
for (const { count, res } of ctx.anchors.values())
|
||||
onAnchor(res, count);
|
||||
return typeof reviver === 'function'
|
||||
? applyReviver.applyReviver(reviver, { '': res }, '', res)
|
||||
: res;
|
||||
}
|
||||
}
|
||||
|
||||
exports.NodeBase = NodeBase;
|
||||
39
bundled-addons/pithos/vendor/yaml/lib/nodes/Pair.js
vendored
Normal file
39
bundled-addons/pithos/vendor/yaml/lib/nodes/Pair.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
var createNode = require('../doc/createNode.js');
|
||||
var stringifyPair = require('../stringify/stringifyPair.js');
|
||||
var addPairToJSMap = require('./addPairToJSMap.js');
|
||||
var identity = require('./identity.js');
|
||||
|
||||
function createPair(key, value, ctx) {
|
||||
const k = createNode.createNode(key, undefined, ctx);
|
||||
const v = createNode.createNode(value, undefined, ctx);
|
||||
return new Pair(k, v);
|
||||
}
|
||||
class Pair {
|
||||
constructor(key, value = null) {
|
||||
Object.defineProperty(this, identity.NODE_TYPE, { value: identity.PAIR });
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
clone(schema) {
|
||||
let { key, value } = this;
|
||||
if (identity.isNode(key))
|
||||
key = key.clone(schema);
|
||||
if (identity.isNode(value))
|
||||
value = value.clone(schema);
|
||||
return new Pair(key, value);
|
||||
}
|
||||
toJSON(_, ctx) {
|
||||
const pair = ctx?.mapAsMap ? new Map() : {};
|
||||
return addPairToJSMap.addPairToJSMap(ctx, pair, this);
|
||||
}
|
||||
toString(ctx, onComment, onChompKeep) {
|
||||
return ctx?.doc
|
||||
? stringifyPair.stringifyPair(this, ctx, onComment, onChompKeep)
|
||||
: JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
|
||||
exports.Pair = Pair;
|
||||
exports.createPair = createPair;
|
||||
27
bundled-addons/pithos/vendor/yaml/lib/nodes/Scalar.js
vendored
Normal file
27
bundled-addons/pithos/vendor/yaml/lib/nodes/Scalar.js
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('./identity.js');
|
||||
var Node = require('./Node.js');
|
||||
var toJS = require('./toJS.js');
|
||||
|
||||
const isScalarValue = (value) => !value || (typeof value !== 'function' && typeof value !== 'object');
|
||||
class Scalar extends Node.NodeBase {
|
||||
constructor(value) {
|
||||
super(identity.SCALAR);
|
||||
this.value = value;
|
||||
}
|
||||
toJSON(arg, ctx) {
|
||||
return ctx?.keep ? this.value : toJS.toJS(this.value, arg, ctx);
|
||||
}
|
||||
toString() {
|
||||
return String(this.value);
|
||||
}
|
||||
}
|
||||
Scalar.BLOCK_FOLDED = 'BLOCK_FOLDED';
|
||||
Scalar.BLOCK_LITERAL = 'BLOCK_LITERAL';
|
||||
Scalar.PLAIN = 'PLAIN';
|
||||
Scalar.QUOTE_DOUBLE = 'QUOTE_DOUBLE';
|
||||
Scalar.QUOTE_SINGLE = 'QUOTE_SINGLE';
|
||||
|
||||
exports.Scalar = Scalar;
|
||||
exports.isScalarValue = isScalarValue;
|
||||
147
bundled-addons/pithos/vendor/yaml/lib/nodes/YAMLMap.js
vendored
Normal file
147
bundled-addons/pithos/vendor/yaml/lib/nodes/YAMLMap.js
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
'use strict';
|
||||
|
||||
var stringifyCollection = require('../stringify/stringifyCollection.js');
|
||||
var addPairToJSMap = require('./addPairToJSMap.js');
|
||||
var Collection = require('./Collection.js');
|
||||
var identity = require('./identity.js');
|
||||
var Pair = require('./Pair.js');
|
||||
var Scalar = require('./Scalar.js');
|
||||
|
||||
function findPair(items, key) {
|
||||
const k = identity.isScalar(key) ? key.value : key;
|
||||
for (const it of items) {
|
||||
if (identity.isPair(it)) {
|
||||
if (it.key === key || it.key === k)
|
||||
return it;
|
||||
if (identity.isScalar(it.key) && it.key.value === k)
|
||||
return it;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
class YAMLMap extends Collection.Collection {
|
||||
static get tagName() {
|
||||
return 'tag:yaml.org,2002:map';
|
||||
}
|
||||
constructor(schema) {
|
||||
super(identity.MAP, schema);
|
||||
this.items = [];
|
||||
}
|
||||
/**
|
||||
* A generic collection parsing method that can be extended
|
||||
* to other node classes that inherit from YAMLMap
|
||||
*/
|
||||
static from(schema, obj, ctx) {
|
||||
const { keepUndefined, replacer } = ctx;
|
||||
const map = new this(schema);
|
||||
const add = (key, value) => {
|
||||
if (typeof replacer === 'function')
|
||||
value = replacer.call(obj, key, value);
|
||||
else if (Array.isArray(replacer) && !replacer.includes(key))
|
||||
return;
|
||||
if (value !== undefined || keepUndefined)
|
||||
map.items.push(Pair.createPair(key, value, ctx));
|
||||
};
|
||||
if (obj instanceof Map) {
|
||||
for (const [key, value] of obj)
|
||||
add(key, value);
|
||||
}
|
||||
else if (obj && typeof obj === 'object') {
|
||||
for (const key of Object.keys(obj))
|
||||
add(key, obj[key]);
|
||||
}
|
||||
if (typeof schema.sortMapEntries === 'function') {
|
||||
map.items.sort(schema.sortMapEntries);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
/**
|
||||
* Adds a value to the collection.
|
||||
*
|
||||
* @param overwrite - If not set `true`, using a key that is already in the
|
||||
* collection will throw. Otherwise, overwrites the previous value.
|
||||
*/
|
||||
add(pair, overwrite) {
|
||||
let _pair;
|
||||
if (identity.isPair(pair))
|
||||
_pair = pair;
|
||||
else if (!pair || typeof pair !== 'object' || !('key' in pair)) {
|
||||
// In TypeScript, this never happens.
|
||||
_pair = new Pair.Pair(pair, pair?.value);
|
||||
}
|
||||
else
|
||||
_pair = new Pair.Pair(pair.key, pair.value);
|
||||
const prev = findPair(this.items, _pair.key);
|
||||
const sortEntries = this.schema?.sortMapEntries;
|
||||
if (prev) {
|
||||
if (!overwrite)
|
||||
throw new Error(`Key ${_pair.key} already set`);
|
||||
// For scalars, keep the old node & its comments and anchors
|
||||
if (identity.isScalar(prev.value) && Scalar.isScalarValue(_pair.value))
|
||||
prev.value.value = _pair.value;
|
||||
else
|
||||
prev.value = _pair.value;
|
||||
}
|
||||
else if (sortEntries) {
|
||||
const i = this.items.findIndex(item => sortEntries(_pair, item) < 0);
|
||||
if (i === -1)
|
||||
this.items.push(_pair);
|
||||
else
|
||||
this.items.splice(i, 0, _pair);
|
||||
}
|
||||
else {
|
||||
this.items.push(_pair);
|
||||
}
|
||||
}
|
||||
delete(key) {
|
||||
const it = findPair(this.items, key);
|
||||
if (!it)
|
||||
return false;
|
||||
const del = this.items.splice(this.items.indexOf(it), 1);
|
||||
return del.length > 0;
|
||||
}
|
||||
get(key, keepScalar) {
|
||||
const it = findPair(this.items, key);
|
||||
const node = it?.value;
|
||||
return (!keepScalar && identity.isScalar(node) ? node.value : node) ?? undefined;
|
||||
}
|
||||
has(key) {
|
||||
return !!findPair(this.items, key);
|
||||
}
|
||||
set(key, value) {
|
||||
this.add(new Pair.Pair(key, value), true);
|
||||
}
|
||||
/**
|
||||
* @param ctx - Conversion context, originally set in Document#toJS()
|
||||
* @param {Class} Type - If set, forces the returned collection type
|
||||
* @returns Instance of Type, Map, or Object
|
||||
*/
|
||||
toJSON(_, ctx, Type) {
|
||||
const map = Type ? new Type() : ctx?.mapAsMap ? new Map() : {};
|
||||
if (ctx?.onCreate)
|
||||
ctx.onCreate(map);
|
||||
for (const item of this.items)
|
||||
addPairToJSMap.addPairToJSMap(ctx, map, item);
|
||||
return map;
|
||||
}
|
||||
toString(ctx, onComment, onChompKeep) {
|
||||
if (!ctx)
|
||||
return JSON.stringify(this);
|
||||
for (const item of this.items) {
|
||||
if (!identity.isPair(item))
|
||||
throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`);
|
||||
}
|
||||
if (!ctx.allNullValues && this.hasAllNullValues(false))
|
||||
ctx = Object.assign({}, ctx, { allNullValues: true });
|
||||
return stringifyCollection.stringifyCollection(this, ctx, {
|
||||
blockItemPrefix: '',
|
||||
flowChars: { start: '{', end: '}' },
|
||||
itemIndent: ctx.indent || '',
|
||||
onChompKeep,
|
||||
onComment
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.YAMLMap = YAMLMap;
|
||||
exports.findPair = findPair;
|
||||
115
bundled-addons/pithos/vendor/yaml/lib/nodes/YAMLSeq.js
vendored
Normal file
115
bundled-addons/pithos/vendor/yaml/lib/nodes/YAMLSeq.js
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
'use strict';
|
||||
|
||||
var createNode = require('../doc/createNode.js');
|
||||
var stringifyCollection = require('../stringify/stringifyCollection.js');
|
||||
var Collection = require('./Collection.js');
|
||||
var identity = require('./identity.js');
|
||||
var Scalar = require('./Scalar.js');
|
||||
var toJS = require('./toJS.js');
|
||||
|
||||
class YAMLSeq extends Collection.Collection {
|
||||
static get tagName() {
|
||||
return 'tag:yaml.org,2002:seq';
|
||||
}
|
||||
constructor(schema) {
|
||||
super(identity.SEQ, schema);
|
||||
this.items = [];
|
||||
}
|
||||
add(value) {
|
||||
this.items.push(value);
|
||||
}
|
||||
/**
|
||||
* Removes a value from the collection.
|
||||
*
|
||||
* `key` must contain a representation of an integer for this to succeed.
|
||||
* It may be wrapped in a `Scalar`.
|
||||
*
|
||||
* @returns `true` if the item was found and removed.
|
||||
*/
|
||||
delete(key) {
|
||||
const idx = asItemIndex(key);
|
||||
if (typeof idx !== 'number')
|
||||
return false;
|
||||
const del = this.items.splice(idx, 1);
|
||||
return del.length > 0;
|
||||
}
|
||||
get(key, keepScalar) {
|
||||
const idx = asItemIndex(key);
|
||||
if (typeof idx !== 'number')
|
||||
return undefined;
|
||||
const it = this.items[idx];
|
||||
return !keepScalar && identity.isScalar(it) ? it.value : it;
|
||||
}
|
||||
/**
|
||||
* Checks if the collection includes a value with the key `key`.
|
||||
*
|
||||
* `key` must contain a representation of an integer for this to succeed.
|
||||
* It may be wrapped in a `Scalar`.
|
||||
*/
|
||||
has(key) {
|
||||
const idx = asItemIndex(key);
|
||||
return typeof idx === 'number' && idx < this.items.length;
|
||||
}
|
||||
/**
|
||||
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
||||
* boolean to add/remove the item from the set.
|
||||
*
|
||||
* If `key` does not contain a representation of an integer, this will throw.
|
||||
* It may be wrapped in a `Scalar`.
|
||||
*/
|
||||
set(key, value) {
|
||||
const idx = asItemIndex(key);
|
||||
if (typeof idx !== 'number')
|
||||
throw new Error(`Expected a valid index, not ${key}.`);
|
||||
const prev = this.items[idx];
|
||||
if (identity.isScalar(prev) && Scalar.isScalarValue(value))
|
||||
prev.value = value;
|
||||
else
|
||||
this.items[idx] = value;
|
||||
}
|
||||
toJSON(_, ctx) {
|
||||
const seq = [];
|
||||
if (ctx?.onCreate)
|
||||
ctx.onCreate(seq);
|
||||
let i = 0;
|
||||
for (const item of this.items)
|
||||
seq.push(toJS.toJS(item, String(i++), ctx));
|
||||
return seq;
|
||||
}
|
||||
toString(ctx, onComment, onChompKeep) {
|
||||
if (!ctx)
|
||||
return JSON.stringify(this);
|
||||
return stringifyCollection.stringifyCollection(this, ctx, {
|
||||
blockItemPrefix: '- ',
|
||||
flowChars: { start: '[', end: ']' },
|
||||
itemIndent: (ctx.indent || '') + ' ',
|
||||
onChompKeep,
|
||||
onComment
|
||||
});
|
||||
}
|
||||
static from(schema, obj, ctx) {
|
||||
const { replacer } = ctx;
|
||||
const seq = new this(schema);
|
||||
if (obj && Symbol.iterator in Object(obj)) {
|
||||
let i = 0;
|
||||
for (let it of obj) {
|
||||
if (typeof replacer === 'function') {
|
||||
const key = obj instanceof Set ? it : String(i++);
|
||||
it = replacer.call(obj, key, it);
|
||||
}
|
||||
seq.items.push(createNode.createNode(it, undefined, ctx));
|
||||
}
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
}
|
||||
function asItemIndex(key) {
|
||||
let idx = identity.isScalar(key) ? key.value : key;
|
||||
if (idx && typeof idx === 'string')
|
||||
idx = Number(idx);
|
||||
return typeof idx === 'number' && Number.isInteger(idx) && idx >= 0
|
||||
? idx
|
||||
: null;
|
||||
}
|
||||
|
||||
exports.YAMLSeq = YAMLSeq;
|
||||
65
bundled-addons/pithos/vendor/yaml/lib/nodes/addPairToJSMap.js
vendored
Normal file
65
bundled-addons/pithos/vendor/yaml/lib/nodes/addPairToJSMap.js
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
'use strict';
|
||||
|
||||
var log = require('../log.js');
|
||||
var merge = require('../schema/yaml-1.1/merge.js');
|
||||
var stringify = require('../stringify/stringify.js');
|
||||
var identity = require('./identity.js');
|
||||
var toJS = require('./toJS.js');
|
||||
|
||||
function addPairToJSMap(ctx, map, { key, value }) {
|
||||
if (identity.isNode(key) && key.addToJSMap)
|
||||
key.addToJSMap(ctx, map, value);
|
||||
// TODO: Should drop this special case for bare << handling
|
||||
else if (merge.isMergeKey(ctx, key))
|
||||
merge.addMergeToJSMap(ctx, map, value);
|
||||
else {
|
||||
const jsKey = toJS.toJS(key, '', ctx);
|
||||
if (map instanceof Map) {
|
||||
map.set(jsKey, toJS.toJS(value, jsKey, ctx));
|
||||
}
|
||||
else if (map instanceof Set) {
|
||||
map.add(jsKey);
|
||||
}
|
||||
else {
|
||||
const stringKey = stringifyKey(key, jsKey, ctx);
|
||||
const jsValue = toJS.toJS(value, stringKey, ctx);
|
||||
if (stringKey in map)
|
||||
Object.defineProperty(map, stringKey, {
|
||||
value: jsValue,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
else
|
||||
map[stringKey] = jsValue;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
function stringifyKey(key, jsKey, ctx) {
|
||||
if (jsKey === null)
|
||||
return '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
if (typeof jsKey !== 'object')
|
||||
return String(jsKey);
|
||||
if (identity.isNode(key) && ctx?.doc) {
|
||||
const strCtx = stringify.createStringifyContext(ctx.doc, {});
|
||||
strCtx.anchors = new Set();
|
||||
for (const node of ctx.anchors.keys())
|
||||
strCtx.anchors.add(node.anchor);
|
||||
strCtx.inFlow = true;
|
||||
strCtx.inStringifyKey = true;
|
||||
const strKey = key.toString(strCtx);
|
||||
if (!ctx.mapKeyWarned) {
|
||||
let jsonStr = JSON.stringify(strKey);
|
||||
if (jsonStr.length > 40)
|
||||
jsonStr = jsonStr.substring(0, 36) + '..."';
|
||||
log.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
|
||||
ctx.mapKeyWarned = true;
|
||||
}
|
||||
return strKey;
|
||||
}
|
||||
return JSON.stringify(jsKey);
|
||||
}
|
||||
|
||||
exports.addPairToJSMap = addPairToJSMap;
|
||||
53
bundled-addons/pithos/vendor/yaml/lib/nodes/identity.js
vendored
Normal file
53
bundled-addons/pithos/vendor/yaml/lib/nodes/identity.js
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
'use strict';
|
||||
|
||||
const ALIAS = Symbol.for('yaml.alias');
|
||||
const DOC = Symbol.for('yaml.document');
|
||||
const MAP = Symbol.for('yaml.map');
|
||||
const PAIR = Symbol.for('yaml.pair');
|
||||
const SCALAR = Symbol.for('yaml.scalar');
|
||||
const SEQ = Symbol.for('yaml.seq');
|
||||
const NODE_TYPE = Symbol.for('yaml.node.type');
|
||||
const isAlias = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === ALIAS;
|
||||
const isDocument = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === DOC;
|
||||
const isMap = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === MAP;
|
||||
const isPair = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === PAIR;
|
||||
const isScalar = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === SCALAR;
|
||||
const isSeq = (node) => !!node && typeof node === 'object' && node[NODE_TYPE] === SEQ;
|
||||
function isCollection(node) {
|
||||
if (node && typeof node === 'object')
|
||||
switch (node[NODE_TYPE]) {
|
||||
case MAP:
|
||||
case SEQ:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isNode(node) {
|
||||
if (node && typeof node === 'object')
|
||||
switch (node[NODE_TYPE]) {
|
||||
case ALIAS:
|
||||
case MAP:
|
||||
case SCALAR:
|
||||
case SEQ:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
|
||||
|
||||
exports.ALIAS = ALIAS;
|
||||
exports.DOC = DOC;
|
||||
exports.MAP = MAP;
|
||||
exports.NODE_TYPE = NODE_TYPE;
|
||||
exports.PAIR = PAIR;
|
||||
exports.SCALAR = SCALAR;
|
||||
exports.SEQ = SEQ;
|
||||
exports.hasAnchor = hasAnchor;
|
||||
exports.isAlias = isAlias;
|
||||
exports.isCollection = isCollection;
|
||||
exports.isDocument = isDocument;
|
||||
exports.isMap = isMap;
|
||||
exports.isNode = isNode;
|
||||
exports.isPair = isPair;
|
||||
exports.isScalar = isScalar;
|
||||
exports.isSeq = isSeq;
|
||||
39
bundled-addons/pithos/vendor/yaml/lib/nodes/toJS.js
vendored
Normal file
39
bundled-addons/pithos/vendor/yaml/lib/nodes/toJS.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('./identity.js');
|
||||
|
||||
/**
|
||||
* Recursively convert any node or its contents to native JavaScript
|
||||
*
|
||||
* @param value - The input value
|
||||
* @param arg - If `value` defines a `toJSON()` method, use this
|
||||
* as its first argument
|
||||
* @param ctx - Conversion context, originally set in Document#toJS(). If
|
||||
* `{ keep: true }` is not set, output should be suitable for JSON
|
||||
* stringification.
|
||||
*/
|
||||
function toJS(value, arg, ctx) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
if (Array.isArray(value))
|
||||
return value.map((v, i) => toJS(v, String(i), ctx));
|
||||
if (value && typeof value.toJSON === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (!ctx || !identity.hasAnchor(value))
|
||||
return value.toJSON(arg, ctx);
|
||||
const data = { aliasCount: 0, count: 1, res: undefined };
|
||||
ctx.anchors.set(value, data);
|
||||
ctx.onCreate = res => {
|
||||
data.res = res;
|
||||
delete ctx.onCreate;
|
||||
};
|
||||
const res = value.toJSON(arg, ctx);
|
||||
if (ctx.onCreate)
|
||||
ctx.onCreate(res);
|
||||
return res;
|
||||
}
|
||||
if (typeof value === 'bigint' && !ctx?.keep)
|
||||
return Number(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
exports.toJS = toJS;
|
||||
218
bundled-addons/pithos/vendor/yaml/lib/parse/cst-scalar.js
vendored
Normal file
218
bundled-addons/pithos/vendor/yaml/lib/parse/cst-scalar.js
vendored
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
'use strict';
|
||||
|
||||
var resolveBlockScalar = require('../compose/resolve-block-scalar.js');
|
||||
var resolveFlowScalar = require('../compose/resolve-flow-scalar.js');
|
||||
var errors = require('../errors.js');
|
||||
var stringifyString = require('../stringify/stringifyString.js');
|
||||
|
||||
function resolveAsScalar(token, strict = true, onError) {
|
||||
if (token) {
|
||||
const _onError = (pos, code, message) => {
|
||||
const offset = typeof pos === 'number' ? pos : Array.isArray(pos) ? pos[0] : pos.offset;
|
||||
if (onError)
|
||||
onError(offset, code, message);
|
||||
else
|
||||
throw new errors.YAMLParseError([offset, offset + 1], code, message);
|
||||
};
|
||||
switch (token.type) {
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar':
|
||||
return resolveFlowScalar.resolveFlowScalar(token, strict, _onError);
|
||||
case 'block-scalar':
|
||||
return resolveBlockScalar.resolveBlockScalar({ options: { strict } }, token, _onError);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Create a new scalar token with `value`
|
||||
*
|
||||
* Values that represent an actual string but may be parsed as a different type should use a `type` other than `'PLAIN'`,
|
||||
* as this function does not support any schema operations and won't check for such conflicts.
|
||||
*
|
||||
* @param value The string representation of the value, which will have its content properly indented.
|
||||
* @param context.end Comments and whitespace after the end of the value, or after the block scalar header. If undefined, a newline will be added.
|
||||
* @param context.implicitKey Being within an implicit key may affect the resolved type of the token's value.
|
||||
* @param context.indent The indent level of the token.
|
||||
* @param context.inFlow Is this scalar within a flow collection? This may affect the resolved type of the token's value.
|
||||
* @param context.offset The offset position of the token.
|
||||
* @param context.type The preferred type of the scalar token. If undefined, the previous type of the `token` will be used, defaulting to `'PLAIN'`.
|
||||
*/
|
||||
function createScalarToken(value, context) {
|
||||
const { implicitKey = false, indent, inFlow = false, offset = -1, type = 'PLAIN' } = context;
|
||||
const source = stringifyString.stringifyString({ type, value }, {
|
||||
implicitKey,
|
||||
indent: indent > 0 ? ' '.repeat(indent) : '',
|
||||
inFlow,
|
||||
options: { blockQuote: true, lineWidth: -1 }
|
||||
});
|
||||
const end = context.end ?? [
|
||||
{ type: 'newline', offset: -1, indent, source: '\n' }
|
||||
];
|
||||
switch (source[0]) {
|
||||
case '|':
|
||||
case '>': {
|
||||
const he = source.indexOf('\n');
|
||||
const head = source.substring(0, he);
|
||||
const body = source.substring(he + 1) + '\n';
|
||||
const props = [
|
||||
{ type: 'block-scalar-header', offset, indent, source: head }
|
||||
];
|
||||
if (!addEndtoBlockProps(props, end))
|
||||
props.push({ type: 'newline', offset: -1, indent, source: '\n' });
|
||||
return { type: 'block-scalar', offset, indent, props, source: body };
|
||||
}
|
||||
case '"':
|
||||
return { type: 'double-quoted-scalar', offset, indent, source, end };
|
||||
case "'":
|
||||
return { type: 'single-quoted-scalar', offset, indent, source, end };
|
||||
default:
|
||||
return { type: 'scalar', offset, indent, source, end };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set the value of `token` to the given string `value`, overwriting any previous contents and type that it may have.
|
||||
*
|
||||
* Best efforts are made to retain any comments previously associated with the `token`,
|
||||
* though all contents within a collection's `items` will be overwritten.
|
||||
*
|
||||
* Values that represent an actual string but may be parsed as a different type should use a `type` other than `'PLAIN'`,
|
||||
* as this function does not support any schema operations and won't check for such conflicts.
|
||||
*
|
||||
* @param token Any token. If it does not include an `indent` value, the value will be stringified as if it were an implicit key.
|
||||
* @param value The string representation of the value, which will have its content properly indented.
|
||||
* @param context.afterKey In most cases, values after a key should have an additional level of indentation.
|
||||
* @param context.implicitKey Being within an implicit key may affect the resolved type of the token's value.
|
||||
* @param context.inFlow Being within a flow collection may affect the resolved type of the token's value.
|
||||
* @param context.type The preferred type of the scalar token. If undefined, the previous type of the `token` will be used, defaulting to `'PLAIN'`.
|
||||
*/
|
||||
function setScalarValue(token, value, context = {}) {
|
||||
let { afterKey = false, implicitKey = false, inFlow = false, type } = context;
|
||||
let indent = 'indent' in token ? token.indent : null;
|
||||
if (afterKey && typeof indent === 'number')
|
||||
indent += 2;
|
||||
if (!type)
|
||||
switch (token.type) {
|
||||
case 'single-quoted-scalar':
|
||||
type = 'QUOTE_SINGLE';
|
||||
break;
|
||||
case 'double-quoted-scalar':
|
||||
type = 'QUOTE_DOUBLE';
|
||||
break;
|
||||
case 'block-scalar': {
|
||||
const header = token.props[0];
|
||||
if (header.type !== 'block-scalar-header')
|
||||
throw new Error('Invalid block scalar header');
|
||||
type = header.source[0] === '>' ? 'BLOCK_FOLDED' : 'BLOCK_LITERAL';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
type = 'PLAIN';
|
||||
}
|
||||
const source = stringifyString.stringifyString({ type, value }, {
|
||||
implicitKey: implicitKey || indent === null,
|
||||
indent: indent !== null && indent > 0 ? ' '.repeat(indent) : '',
|
||||
inFlow,
|
||||
options: { blockQuote: true, lineWidth: -1 }
|
||||
});
|
||||
switch (source[0]) {
|
||||
case '|':
|
||||
case '>':
|
||||
setBlockScalarValue(token, source);
|
||||
break;
|
||||
case '"':
|
||||
setFlowScalarValue(token, source, 'double-quoted-scalar');
|
||||
break;
|
||||
case "'":
|
||||
setFlowScalarValue(token, source, 'single-quoted-scalar');
|
||||
break;
|
||||
default:
|
||||
setFlowScalarValue(token, source, 'scalar');
|
||||
}
|
||||
}
|
||||
function setBlockScalarValue(token, source) {
|
||||
const he = source.indexOf('\n');
|
||||
const head = source.substring(0, he);
|
||||
const body = source.substring(he + 1) + '\n';
|
||||
if (token.type === 'block-scalar') {
|
||||
const header = token.props[0];
|
||||
if (header.type !== 'block-scalar-header')
|
||||
throw new Error('Invalid block scalar header');
|
||||
header.source = head;
|
||||
token.source = body;
|
||||
}
|
||||
else {
|
||||
const { offset } = token;
|
||||
const indent = 'indent' in token ? token.indent : -1;
|
||||
const props = [
|
||||
{ type: 'block-scalar-header', offset, indent, source: head }
|
||||
];
|
||||
if (!addEndtoBlockProps(props, 'end' in token ? token.end : undefined))
|
||||
props.push({ type: 'newline', offset: -1, indent, source: '\n' });
|
||||
for (const key of Object.keys(token))
|
||||
if (key !== 'type' && key !== 'offset')
|
||||
delete token[key];
|
||||
Object.assign(token, { type: 'block-scalar', indent, props, source: body });
|
||||
}
|
||||
}
|
||||
/** @returns `true` if last token is a newline */
|
||||
function addEndtoBlockProps(props, end) {
|
||||
if (end)
|
||||
for (const st of end)
|
||||
switch (st.type) {
|
||||
case 'space':
|
||||
case 'comment':
|
||||
props.push(st);
|
||||
break;
|
||||
case 'newline':
|
||||
props.push(st);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function setFlowScalarValue(token, source, type) {
|
||||
switch (token.type) {
|
||||
case 'scalar':
|
||||
case 'double-quoted-scalar':
|
||||
case 'single-quoted-scalar':
|
||||
token.type = type;
|
||||
token.source = source;
|
||||
break;
|
||||
case 'block-scalar': {
|
||||
const end = token.props.slice(1);
|
||||
let oa = source.length;
|
||||
if (token.props[0].type === 'block-scalar-header')
|
||||
oa -= token.props[0].source.length;
|
||||
for (const tok of end)
|
||||
tok.offset += oa;
|
||||
delete token.props;
|
||||
Object.assign(token, { type, source, end });
|
||||
break;
|
||||
}
|
||||
case 'block-map':
|
||||
case 'block-seq': {
|
||||
const offset = token.offset + source.length;
|
||||
const nl = { type: 'newline', offset, indent: token.indent, source: '\n' };
|
||||
delete token.items;
|
||||
Object.assign(token, { type, source, end: [nl] });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const indent = 'indent' in token ? token.indent : -1;
|
||||
const end = 'end' in token && Array.isArray(token.end)
|
||||
? token.end.filter(st => st.type === 'space' ||
|
||||
st.type === 'comment' ||
|
||||
st.type === 'newline')
|
||||
: [];
|
||||
for (const key of Object.keys(token))
|
||||
if (key !== 'type' && key !== 'offset')
|
||||
delete token[key];
|
||||
Object.assign(token, { type, indent, source, end });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.createScalarToken = createScalarToken;
|
||||
exports.resolveAsScalar = resolveAsScalar;
|
||||
exports.setScalarValue = setScalarValue;
|
||||
63
bundled-addons/pithos/vendor/yaml/lib/parse/cst-stringify.js
vendored
Normal file
63
bundled-addons/pithos/vendor/yaml/lib/parse/cst-stringify.js
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Stringify a CST document, token, or collection item
|
||||
*
|
||||
* Fair warning: This applies no validation whatsoever, and
|
||||
* simply concatenates the sources in their logical order.
|
||||
*/
|
||||
const stringify = (cst) => 'type' in cst ? stringifyToken(cst) : stringifyItem(cst);
|
||||
function stringifyToken(token) {
|
||||
switch (token.type) {
|
||||
case 'block-scalar': {
|
||||
let res = '';
|
||||
for (const tok of token.props)
|
||||
res += stringifyToken(tok);
|
||||
return res + token.source;
|
||||
}
|
||||
case 'block-map':
|
||||
case 'block-seq': {
|
||||
let res = '';
|
||||
for (const item of token.items)
|
||||
res += stringifyItem(item);
|
||||
return res;
|
||||
}
|
||||
case 'flow-collection': {
|
||||
let res = token.start.source;
|
||||
for (const item of token.items)
|
||||
res += stringifyItem(item);
|
||||
for (const st of token.end)
|
||||
res += st.source;
|
||||
return res;
|
||||
}
|
||||
case 'document': {
|
||||
let res = stringifyItem(token);
|
||||
if (token.end)
|
||||
for (const st of token.end)
|
||||
res += st.source;
|
||||
return res;
|
||||
}
|
||||
default: {
|
||||
let res = token.source;
|
||||
if ('end' in token && token.end)
|
||||
for (const st of token.end)
|
||||
res += st.source;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
function stringifyItem({ start, key, sep, value }) {
|
||||
let res = '';
|
||||
for (const st of start)
|
||||
res += st.source;
|
||||
if (key)
|
||||
res += stringifyToken(key);
|
||||
if (sep)
|
||||
for (const st of sep)
|
||||
res += st.source;
|
||||
if (value)
|
||||
res += stringifyToken(value);
|
||||
return res;
|
||||
}
|
||||
|
||||
exports.stringify = stringify;
|
||||
99
bundled-addons/pithos/vendor/yaml/lib/parse/cst-visit.js
vendored
Normal file
99
bundled-addons/pithos/vendor/yaml/lib/parse/cst-visit.js
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
const BREAK = Symbol('break visit');
|
||||
const SKIP = Symbol('skip children');
|
||||
const REMOVE = Symbol('remove item');
|
||||
/**
|
||||
* Apply a visitor to a CST document or item.
|
||||
*
|
||||
* Walks through the tree (depth-first) starting from the root, calling a
|
||||
* `visitor` function with two arguments when entering each item:
|
||||
* - `item`: The current item, which included the following members:
|
||||
* - `start: SourceToken[]` – Source tokens before the key or value,
|
||||
* possibly including its anchor or tag.
|
||||
* - `key?: Token | null` – Set for pair values. May then be `null`, if
|
||||
* the key before the `:` separator is empty.
|
||||
* - `sep?: SourceToken[]` – Source tokens between the key and the value,
|
||||
* which should include the `:` map value indicator if `value` is set.
|
||||
* - `value?: Token` – The value of a sequence item, or of a map pair.
|
||||
* - `path`: The steps from the root to the current node, as an array of
|
||||
* `['key' | 'value', number]` tuples.
|
||||
*
|
||||
* The return value of the visitor may be used to control the traversal:
|
||||
* - `undefined` (default): Do nothing and continue
|
||||
* - `visit.SKIP`: Do not visit the children of this token, continue with
|
||||
* next sibling
|
||||
* - `visit.BREAK`: Terminate traversal completely
|
||||
* - `visit.REMOVE`: Remove the current item, then continue with the next one
|
||||
* - `number`: Set the index of the next step. This is useful especially if
|
||||
* the index of the current token has changed.
|
||||
* - `function`: Define the next visitor for this item. After the original
|
||||
* visitor is called on item entry, next visitors are called after handling
|
||||
* a non-empty `key` and when exiting the item.
|
||||
*/
|
||||
function visit(cst, visitor) {
|
||||
if ('type' in cst && cst.type === 'document')
|
||||
cst = { start: cst.start, value: cst.value };
|
||||
_visit(Object.freeze([]), cst, visitor);
|
||||
}
|
||||
// Without the `as symbol` casts, TS declares these in the `visit`
|
||||
// namespace using `var`, but then complains about that because
|
||||
// `unique symbol` must be `const`.
|
||||
/** Terminate visit traversal completely */
|
||||
visit.BREAK = BREAK;
|
||||
/** Do not visit the children of the current item */
|
||||
visit.SKIP = SKIP;
|
||||
/** Remove the current item */
|
||||
visit.REMOVE = REMOVE;
|
||||
/** Find the item at `path` from `cst` as the root */
|
||||
visit.itemAtPath = (cst, path) => {
|
||||
let item = cst;
|
||||
for (const [field, index] of path) {
|
||||
const tok = item?.[field];
|
||||
if (tok && 'items' in tok) {
|
||||
item = tok.items[index];
|
||||
}
|
||||
else
|
||||
return undefined;
|
||||
}
|
||||
return item;
|
||||
};
|
||||
/**
|
||||
* Get the immediate parent collection of the item at `path` from `cst` as the root.
|
||||
*
|
||||
* Throws an error if the collection is not found, which should never happen if the item itself exists.
|
||||
*/
|
||||
visit.parentCollection = (cst, path) => {
|
||||
const parent = visit.itemAtPath(cst, path.slice(0, -1));
|
||||
const field = path[path.length - 1][0];
|
||||
const coll = parent?.[field];
|
||||
if (coll && 'items' in coll)
|
||||
return coll;
|
||||
throw new Error('Parent collection not found');
|
||||
};
|
||||
function _visit(path, item, visitor) {
|
||||
let ctrl = visitor(item, path);
|
||||
if (typeof ctrl === 'symbol')
|
||||
return ctrl;
|
||||
for (const field of ['key', 'value']) {
|
||||
const token = item[field];
|
||||
if (token && 'items' in token) {
|
||||
for (let i = 0; i < token.items.length; ++i) {
|
||||
const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor);
|
||||
if (typeof ci === 'number')
|
||||
i = ci - 1;
|
||||
else if (ci === BREAK)
|
||||
return BREAK;
|
||||
else if (ci === REMOVE) {
|
||||
token.items.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
if (typeof ctrl === 'function' && field === 'key')
|
||||
ctrl = ctrl(item, path);
|
||||
}
|
||||
}
|
||||
return typeof ctrl === 'function' ? ctrl(item, path) : ctrl;
|
||||
}
|
||||
|
||||
exports.visit = visit;
|
||||
112
bundled-addons/pithos/vendor/yaml/lib/parse/cst.js
vendored
Normal file
112
bundled-addons/pithos/vendor/yaml/lib/parse/cst.js
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
'use strict';
|
||||
|
||||
var cstScalar = require('./cst-scalar.js');
|
||||
var cstStringify = require('./cst-stringify.js');
|
||||
var cstVisit = require('./cst-visit.js');
|
||||
|
||||
/** The byte order mark */
|
||||
const BOM = '\u{FEFF}';
|
||||
/** Start of doc-mode */
|
||||
const DOCUMENT = '\x02'; // C0: Start of Text
|
||||
/** Unexpected end of flow-mode */
|
||||
const FLOW_END = '\x18'; // C0: Cancel
|
||||
/** Next token is a scalar value */
|
||||
const SCALAR = '\x1f'; // C0: Unit Separator
|
||||
/** @returns `true` if `token` is a flow or block collection */
|
||||
const isCollection = (token) => !!token && 'items' in token;
|
||||
/** @returns `true` if `token` is a flow or block scalar; not an alias */
|
||||
const isScalar = (token) => !!token &&
|
||||
(token.type === 'scalar' ||
|
||||
token.type === 'single-quoted-scalar' ||
|
||||
token.type === 'double-quoted-scalar' ||
|
||||
token.type === 'block-scalar');
|
||||
/* istanbul ignore next */
|
||||
/** Get a printable representation of a lexer token */
|
||||
function prettyToken(token) {
|
||||
switch (token) {
|
||||
case BOM:
|
||||
return '<BOM>';
|
||||
case DOCUMENT:
|
||||
return '<DOC>';
|
||||
case FLOW_END:
|
||||
return '<FLOW_END>';
|
||||
case SCALAR:
|
||||
return '<SCALAR>';
|
||||
default:
|
||||
return JSON.stringify(token);
|
||||
}
|
||||
}
|
||||
/** Identify the type of a lexer token. May return `null` for unknown tokens. */
|
||||
function tokenType(source) {
|
||||
switch (source) {
|
||||
case BOM:
|
||||
return 'byte-order-mark';
|
||||
case DOCUMENT:
|
||||
return 'doc-mode';
|
||||
case FLOW_END:
|
||||
return 'flow-error-end';
|
||||
case SCALAR:
|
||||
return 'scalar';
|
||||
case '---':
|
||||
return 'doc-start';
|
||||
case '...':
|
||||
return 'doc-end';
|
||||
case '':
|
||||
case '\n':
|
||||
case '\r\n':
|
||||
return 'newline';
|
||||
case '-':
|
||||
return 'seq-item-ind';
|
||||
case '?':
|
||||
return 'explicit-key-ind';
|
||||
case ':':
|
||||
return 'map-value-ind';
|
||||
case '{':
|
||||
return 'flow-map-start';
|
||||
case '}':
|
||||
return 'flow-map-end';
|
||||
case '[':
|
||||
return 'flow-seq-start';
|
||||
case ']':
|
||||
return 'flow-seq-end';
|
||||
case ',':
|
||||
return 'comma';
|
||||
}
|
||||
switch (source[0]) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
return 'space';
|
||||
case '#':
|
||||
return 'comment';
|
||||
case '%':
|
||||
return 'directive-line';
|
||||
case '*':
|
||||
return 'alias';
|
||||
case '&':
|
||||
return 'anchor';
|
||||
case '!':
|
||||
return 'tag';
|
||||
case "'":
|
||||
return 'single-quoted-scalar';
|
||||
case '"':
|
||||
return 'double-quoted-scalar';
|
||||
case '|':
|
||||
case '>':
|
||||
return 'block-scalar-header';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
exports.createScalarToken = cstScalar.createScalarToken;
|
||||
exports.resolveAsScalar = cstScalar.resolveAsScalar;
|
||||
exports.setScalarValue = cstScalar.setScalarValue;
|
||||
exports.stringify = cstStringify.stringify;
|
||||
exports.visit = cstVisit.visit;
|
||||
exports.BOM = BOM;
|
||||
exports.DOCUMENT = DOCUMENT;
|
||||
exports.FLOW_END = FLOW_END;
|
||||
exports.SCALAR = SCALAR;
|
||||
exports.isCollection = isCollection;
|
||||
exports.isScalar = isScalar;
|
||||
exports.prettyToken = prettyToken;
|
||||
exports.tokenType = tokenType;
|
||||
723
bundled-addons/pithos/vendor/yaml/lib/parse/lexer.js
vendored
Normal file
723
bundled-addons/pithos/vendor/yaml/lib/parse/lexer.js
vendored
Normal file
|
|
@ -0,0 +1,723 @@
|
|||
'use strict';
|
||||
|
||||
var cst = require('./cst.js');
|
||||
|
||||
/*
|
||||
START -> stream
|
||||
|
||||
stream
|
||||
directive -> line-end -> stream
|
||||
indent + line-end -> stream
|
||||
[else] -> line-start
|
||||
|
||||
line-end
|
||||
comment -> line-end
|
||||
newline -> .
|
||||
input-end -> END
|
||||
|
||||
line-start
|
||||
doc-start -> doc
|
||||
doc-end -> stream
|
||||
[else] -> indent -> block-start
|
||||
|
||||
block-start
|
||||
seq-item-start -> block-start
|
||||
explicit-key-start -> block-start
|
||||
map-value-start -> block-start
|
||||
[else] -> doc
|
||||
|
||||
doc
|
||||
line-end -> line-start
|
||||
spaces -> doc
|
||||
anchor -> doc
|
||||
tag -> doc
|
||||
flow-start -> flow -> doc
|
||||
flow-end -> error -> doc
|
||||
seq-item-start -> error -> doc
|
||||
explicit-key-start -> error -> doc
|
||||
map-value-start -> doc
|
||||
alias -> doc
|
||||
quote-start -> quoted-scalar -> doc
|
||||
block-scalar-header -> line-end -> block-scalar(min) -> line-start
|
||||
[else] -> plain-scalar(false, min) -> doc
|
||||
|
||||
flow
|
||||
line-end -> flow
|
||||
spaces -> flow
|
||||
anchor -> flow
|
||||
tag -> flow
|
||||
flow-start -> flow -> flow
|
||||
flow-end -> .
|
||||
seq-item-start -> error -> flow
|
||||
explicit-key-start -> flow
|
||||
map-value-start -> flow
|
||||
alias -> flow
|
||||
quote-start -> quoted-scalar -> flow
|
||||
comma -> flow
|
||||
[else] -> plain-scalar(true, 0) -> flow
|
||||
|
||||
quoted-scalar
|
||||
quote-end -> .
|
||||
[else] -> quoted-scalar
|
||||
|
||||
block-scalar(min)
|
||||
newline + peek(indent < min) -> .
|
||||
[else] -> block-scalar(min)
|
||||
|
||||
plain-scalar(is-flow, min)
|
||||
scalar-end(is-flow) -> .
|
||||
peek(newline + (indent < min)) -> .
|
||||
[else] -> plain-scalar(min)
|
||||
*/
|
||||
function isEmpty(ch) {
|
||||
switch (ch) {
|
||||
case undefined:
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const hexDigits = new Set('0123456789ABCDEFabcdef');
|
||||
const tagChars = new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()");
|
||||
const flowIndicatorChars = new Set(',[]{}');
|
||||
const invalidAnchorChars = new Set(' ,[]{}\n\r\t');
|
||||
const isNotAnchorChar = (ch) => !ch || invalidAnchorChars.has(ch);
|
||||
/**
|
||||
* Splits an input string into lexical tokens, i.e. smaller strings that are
|
||||
* easily identifiable by `tokens.tokenType()`.
|
||||
*
|
||||
* Lexing starts always in a "stream" context. Incomplete input may be buffered
|
||||
* until a complete token can be emitted.
|
||||
*
|
||||
* In addition to slices of the original input, the following control characters
|
||||
* may also be emitted:
|
||||
*
|
||||
* - `\x02` (Start of Text): A document starts with the next token
|
||||
* - `\x18` (Cancel): Unexpected end of flow-mode (indicates an error)
|
||||
* - `\x1f` (Unit Separator): Next token is a scalar value
|
||||
* - `\u{FEFF}` (Byte order mark): Emitted separately outside documents
|
||||
*/
|
||||
class Lexer {
|
||||
constructor() {
|
||||
/**
|
||||
* Flag indicating whether the end of the current buffer marks the end of
|
||||
* all input
|
||||
*/
|
||||
this.atEnd = false;
|
||||
/**
|
||||
* Explicit indent set in block scalar header, as an offset from the current
|
||||
* minimum indent, so e.g. set to 1 from a header `|2+`. Set to -1 if not
|
||||
* explicitly set.
|
||||
*/
|
||||
this.blockScalarIndent = -1;
|
||||
/**
|
||||
* Block scalars that include a + (keep) chomping indicator in their header
|
||||
* include trailing empty lines, which are otherwise excluded from the
|
||||
* scalar's contents.
|
||||
*/
|
||||
this.blockScalarKeep = false;
|
||||
/** Current input */
|
||||
this.buffer = '';
|
||||
/**
|
||||
* Flag noting whether the map value indicator : can immediately follow this
|
||||
* node within a flow context.
|
||||
*/
|
||||
this.flowKey = false;
|
||||
/** Count of surrounding flow collection levels. */
|
||||
this.flowLevel = 0;
|
||||
/**
|
||||
* Minimum level of indentation required for next lines to be parsed as a
|
||||
* part of the current scalar value.
|
||||
*/
|
||||
this.indentNext = 0;
|
||||
/** Indentation level of the current line. */
|
||||
this.indentValue = 0;
|
||||
/** Position of the next \n character. */
|
||||
this.lineEndPos = null;
|
||||
/** Stores the state of the lexer if reaching the end of incpomplete input */
|
||||
this.next = null;
|
||||
/** A pointer to `buffer`; the current position of the lexer. */
|
||||
this.pos = 0;
|
||||
}
|
||||
/**
|
||||
* Generate YAML tokens from the `source` string. If `incomplete`,
|
||||
* a part of the last line may be left as a buffer for the next call.
|
||||
*
|
||||
* @returns A generator of lexical tokens
|
||||
*/
|
||||
*lex(source, incomplete = false) {
|
||||
if (source) {
|
||||
if (typeof source !== 'string')
|
||||
throw TypeError('source is not a string');
|
||||
this.buffer = this.buffer ? this.buffer + source : source;
|
||||
this.lineEndPos = null;
|
||||
}
|
||||
this.atEnd = !incomplete;
|
||||
let next = this.next ?? 'stream';
|
||||
while (next && (incomplete || this.hasChars(1)))
|
||||
next = yield* this.parseNext(next);
|
||||
}
|
||||
atLineEnd() {
|
||||
let i = this.pos;
|
||||
let ch = this.buffer[i];
|
||||
while (ch === ' ' || ch === '\t')
|
||||
ch = this.buffer[++i];
|
||||
if (!ch || ch === '#' || ch === '\n')
|
||||
return true;
|
||||
if (ch === '\r')
|
||||
return this.buffer[i + 1] === '\n';
|
||||
return false;
|
||||
}
|
||||
charAt(n) {
|
||||
return this.buffer[this.pos + n];
|
||||
}
|
||||
continueScalar(offset) {
|
||||
let ch = this.buffer[offset];
|
||||
if (this.indentNext > 0) {
|
||||
let indent = 0;
|
||||
while (ch === ' ')
|
||||
ch = this.buffer[++indent + offset];
|
||||
if (ch === '\r') {
|
||||
const next = this.buffer[indent + offset + 1];
|
||||
if (next === '\n' || (!next && !this.atEnd))
|
||||
return offset + indent + 1;
|
||||
}
|
||||
return ch === '\n' || indent >= this.indentNext || (!ch && !this.atEnd)
|
||||
? offset + indent
|
||||
: -1;
|
||||
}
|
||||
if (ch === '-' || ch === '.') {
|
||||
const dt = this.buffer.substr(offset, 3);
|
||||
if ((dt === '---' || dt === '...') && isEmpty(this.buffer[offset + 3]))
|
||||
return -1;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
getLine() {
|
||||
let end = this.lineEndPos;
|
||||
if (typeof end !== 'number' || (end !== -1 && end < this.pos)) {
|
||||
end = this.buffer.indexOf('\n', this.pos);
|
||||
this.lineEndPos = end;
|
||||
}
|
||||
if (end === -1)
|
||||
return this.atEnd ? this.buffer.substring(this.pos) : null;
|
||||
if (this.buffer[end - 1] === '\r')
|
||||
end -= 1;
|
||||
return this.buffer.substring(this.pos, end);
|
||||
}
|
||||
hasChars(n) {
|
||||
return this.pos + n <= this.buffer.length;
|
||||
}
|
||||
setNext(state) {
|
||||
this.buffer = this.buffer.substring(this.pos);
|
||||
this.pos = 0;
|
||||
this.lineEndPos = null;
|
||||
this.next = state;
|
||||
return null;
|
||||
}
|
||||
peek(n) {
|
||||
return this.buffer.substr(this.pos, n);
|
||||
}
|
||||
*parseNext(next) {
|
||||
switch (next) {
|
||||
case 'stream':
|
||||
return yield* this.parseStream();
|
||||
case 'line-start':
|
||||
return yield* this.parseLineStart();
|
||||
case 'block-start':
|
||||
return yield* this.parseBlockStart();
|
||||
case 'doc':
|
||||
return yield* this.parseDocument();
|
||||
case 'flow':
|
||||
return yield* this.parseFlowCollection();
|
||||
case 'quoted-scalar':
|
||||
return yield* this.parseQuotedScalar();
|
||||
case 'block-scalar':
|
||||
return yield* this.parseBlockScalar();
|
||||
case 'plain-scalar':
|
||||
return yield* this.parsePlainScalar();
|
||||
}
|
||||
}
|
||||
*parseStream() {
|
||||
let line = this.getLine();
|
||||
if (line === null)
|
||||
return this.setNext('stream');
|
||||
if (line[0] === cst.BOM) {
|
||||
yield* this.pushCount(1);
|
||||
line = line.substring(1);
|
||||
}
|
||||
if (line[0] === '%') {
|
||||
let dirEnd = line.length;
|
||||
let cs = line.indexOf('#');
|
||||
while (cs !== -1) {
|
||||
const ch = line[cs - 1];
|
||||
if (ch === ' ' || ch === '\t') {
|
||||
dirEnd = cs - 1;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
cs = line.indexOf('#', cs + 1);
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
const ch = line[dirEnd - 1];
|
||||
if (ch === ' ' || ch === '\t')
|
||||
dirEnd -= 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
const n = (yield* this.pushCount(dirEnd)) + (yield* this.pushSpaces(true));
|
||||
yield* this.pushCount(line.length - n); // possible comment
|
||||
this.pushNewline();
|
||||
return 'stream';
|
||||
}
|
||||
if (this.atLineEnd()) {
|
||||
const sp = yield* this.pushSpaces(true);
|
||||
yield* this.pushCount(line.length - sp);
|
||||
yield* this.pushNewline();
|
||||
return 'stream';
|
||||
}
|
||||
yield cst.DOCUMENT;
|
||||
return yield* this.parseLineStart();
|
||||
}
|
||||
*parseLineStart() {
|
||||
const ch = this.charAt(0);
|
||||
if (!ch && !this.atEnd)
|
||||
return this.setNext('line-start');
|
||||
if (ch === '-' || ch === '.') {
|
||||
if (!this.atEnd && !this.hasChars(4))
|
||||
return this.setNext('line-start');
|
||||
const s = this.peek(3);
|
||||
if ((s === '---' || s === '...') && isEmpty(this.charAt(3))) {
|
||||
yield* this.pushCount(3);
|
||||
this.indentValue = 0;
|
||||
this.indentNext = 0;
|
||||
return s === '---' ? 'doc' : 'stream';
|
||||
}
|
||||
}
|
||||
this.indentValue = yield* this.pushSpaces(false);
|
||||
if (this.indentNext > this.indentValue && !isEmpty(this.charAt(1)))
|
||||
this.indentNext = this.indentValue;
|
||||
return yield* this.parseBlockStart();
|
||||
}
|
||||
*parseBlockStart() {
|
||||
const [ch0, ch1] = this.peek(2);
|
||||
if (!ch1 && !this.atEnd)
|
||||
return this.setNext('block-start');
|
||||
if ((ch0 === '-' || ch0 === '?' || ch0 === ':') && isEmpty(ch1)) {
|
||||
const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true));
|
||||
this.indentNext = this.indentValue + 1;
|
||||
this.indentValue += n;
|
||||
return 'block-start';
|
||||
}
|
||||
return 'doc';
|
||||
}
|
||||
*parseDocument() {
|
||||
yield* this.pushSpaces(true);
|
||||
const line = this.getLine();
|
||||
if (line === null)
|
||||
return this.setNext('doc');
|
||||
let n = yield* this.pushIndicators();
|
||||
switch (line[n]) {
|
||||
case '#':
|
||||
yield* this.pushCount(line.length - n);
|
||||
// fallthrough
|
||||
case undefined:
|
||||
yield* this.pushNewline();
|
||||
return yield* this.parseLineStart();
|
||||
case '{':
|
||||
case '[':
|
||||
yield* this.pushCount(1);
|
||||
this.flowKey = false;
|
||||
this.flowLevel = 1;
|
||||
return 'flow';
|
||||
case '}':
|
||||
case ']':
|
||||
// this is an error
|
||||
yield* this.pushCount(1);
|
||||
return 'doc';
|
||||
case '*':
|
||||
yield* this.pushUntil(isNotAnchorChar);
|
||||
return 'doc';
|
||||
case '"':
|
||||
case "'":
|
||||
return yield* this.parseQuotedScalar();
|
||||
case '|':
|
||||
case '>':
|
||||
n += yield* this.parseBlockScalarHeader();
|
||||
n += yield* this.pushSpaces(true);
|
||||
yield* this.pushCount(line.length - n);
|
||||
yield* this.pushNewline();
|
||||
return yield* this.parseBlockScalar();
|
||||
default:
|
||||
return yield* this.parsePlainScalar();
|
||||
}
|
||||
}
|
||||
*parseFlowCollection() {
|
||||
let nl, sp;
|
||||
let indent = -1;
|
||||
do {
|
||||
nl = yield* this.pushNewline();
|
||||
if (nl > 0) {
|
||||
sp = yield* this.pushSpaces(false);
|
||||
this.indentValue = indent = sp;
|
||||
}
|
||||
else {
|
||||
sp = 0;
|
||||
}
|
||||
sp += yield* this.pushSpaces(true);
|
||||
} while (nl + sp > 0);
|
||||
const line = this.getLine();
|
||||
if (line === null)
|
||||
return this.setNext('flow');
|
||||
if ((indent !== -1 && indent < this.indentNext && line[0] !== '#') ||
|
||||
(indent === 0 &&
|
||||
(line.startsWith('---') || line.startsWith('...')) &&
|
||||
isEmpty(line[3]))) {
|
||||
// Allowing for the terminal ] or } at the same (rather than greater)
|
||||
// indent level as the initial [ or { is technically invalid, but
|
||||
// failing here would be surprising to users.
|
||||
const atFlowEndMarker = indent === this.indentNext - 1 &&
|
||||
this.flowLevel === 1 &&
|
||||
(line[0] === ']' || line[0] === '}');
|
||||
if (!atFlowEndMarker) {
|
||||
// this is an error
|
||||
this.flowLevel = 0;
|
||||
yield cst.FLOW_END;
|
||||
return yield* this.parseLineStart();
|
||||
}
|
||||
}
|
||||
let n = 0;
|
||||
while (line[n] === ',') {
|
||||
n += yield* this.pushCount(1);
|
||||
n += yield* this.pushSpaces(true);
|
||||
this.flowKey = false;
|
||||
}
|
||||
n += yield* this.pushIndicators();
|
||||
switch (line[n]) {
|
||||
case undefined:
|
||||
return 'flow';
|
||||
case '#':
|
||||
yield* this.pushCount(line.length - n);
|
||||
return 'flow';
|
||||
case '{':
|
||||
case '[':
|
||||
yield* this.pushCount(1);
|
||||
this.flowKey = false;
|
||||
this.flowLevel += 1;
|
||||
return 'flow';
|
||||
case '}':
|
||||
case ']':
|
||||
yield* this.pushCount(1);
|
||||
this.flowKey = true;
|
||||
this.flowLevel -= 1;
|
||||
return this.flowLevel ? 'flow' : 'doc';
|
||||
case '*':
|
||||
yield* this.pushUntil(isNotAnchorChar);
|
||||
return 'flow';
|
||||
case '"':
|
||||
case "'":
|
||||
this.flowKey = true;
|
||||
return yield* this.parseQuotedScalar();
|
||||
case ':': {
|
||||
const next = this.charAt(1);
|
||||
if (this.flowKey || isEmpty(next) || next === ',') {
|
||||
this.flowKey = false;
|
||||
yield* this.pushCount(1);
|
||||
yield* this.pushSpaces(true);
|
||||
return 'flow';
|
||||
}
|
||||
}
|
||||
// fallthrough
|
||||
default:
|
||||
this.flowKey = false;
|
||||
return yield* this.parsePlainScalar();
|
||||
}
|
||||
}
|
||||
*parseQuotedScalar() {
|
||||
const quote = this.charAt(0);
|
||||
let end = this.buffer.indexOf(quote, this.pos + 1);
|
||||
if (quote === "'") {
|
||||
while (end !== -1 && this.buffer[end + 1] === "'")
|
||||
end = this.buffer.indexOf("'", end + 2);
|
||||
}
|
||||
else {
|
||||
// double-quote
|
||||
while (end !== -1) {
|
||||
let n = 0;
|
||||
while (this.buffer[end - 1 - n] === '\\')
|
||||
n += 1;
|
||||
if (n % 2 === 0)
|
||||
break;
|
||||
end = this.buffer.indexOf('"', end + 1);
|
||||
}
|
||||
}
|
||||
// Only looking for newlines within the quotes
|
||||
const qb = this.buffer.substring(0, end);
|
||||
let nl = qb.indexOf('\n', this.pos);
|
||||
if (nl !== -1) {
|
||||
while (nl !== -1) {
|
||||
const cs = this.continueScalar(nl + 1);
|
||||
if (cs === -1)
|
||||
break;
|
||||
nl = qb.indexOf('\n', cs);
|
||||
}
|
||||
if (nl !== -1) {
|
||||
// this is an error caused by an unexpected unindent
|
||||
end = nl - (qb[nl - 1] === '\r' ? 2 : 1);
|
||||
}
|
||||
}
|
||||
if (end === -1) {
|
||||
if (!this.atEnd)
|
||||
return this.setNext('quoted-scalar');
|
||||
end = this.buffer.length;
|
||||
}
|
||||
yield* this.pushToIndex(end + 1, false);
|
||||
return this.flowLevel ? 'flow' : 'doc';
|
||||
}
|
||||
*parseBlockScalarHeader() {
|
||||
this.blockScalarIndent = -1;
|
||||
this.blockScalarKeep = false;
|
||||
let i = this.pos;
|
||||
while (true) {
|
||||
const ch = this.buffer[++i];
|
||||
if (ch === '+')
|
||||
this.blockScalarKeep = true;
|
||||
else if (ch > '0' && ch <= '9')
|
||||
this.blockScalarIndent = Number(ch) - 1;
|
||||
else if (ch !== '-')
|
||||
break;
|
||||
}
|
||||
return yield* this.pushUntil(ch => isEmpty(ch) || ch === '#');
|
||||
}
|
||||
*parseBlockScalar() {
|
||||
let nl = this.pos - 1; // may be -1 if this.pos === 0
|
||||
let indent = 0;
|
||||
let ch;
|
||||
loop: for (let i = this.pos; (ch = this.buffer[i]); ++i) {
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
indent += 1;
|
||||
break;
|
||||
case '\n':
|
||||
nl = i;
|
||||
indent = 0;
|
||||
break;
|
||||
case '\r': {
|
||||
const next = this.buffer[i + 1];
|
||||
if (!next && !this.atEnd)
|
||||
return this.setNext('block-scalar');
|
||||
if (next === '\n')
|
||||
break;
|
||||
} // fallthrough
|
||||
default:
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
if (!ch && !this.atEnd)
|
||||
return this.setNext('block-scalar');
|
||||
if (indent >= this.indentNext) {
|
||||
if (this.blockScalarIndent === -1)
|
||||
this.indentNext = indent;
|
||||
else {
|
||||
this.indentNext =
|
||||
this.blockScalarIndent + (this.indentNext === 0 ? 1 : this.indentNext);
|
||||
}
|
||||
do {
|
||||
const cs = this.continueScalar(nl + 1);
|
||||
if (cs === -1)
|
||||
break;
|
||||
nl = this.buffer.indexOf('\n', cs);
|
||||
} while (nl !== -1);
|
||||
if (nl === -1) {
|
||||
if (!this.atEnd)
|
||||
return this.setNext('block-scalar');
|
||||
nl = this.buffer.length;
|
||||
}
|
||||
}
|
||||
// Trailing insufficiently indented tabs are invalid.
|
||||
// To catch that during parsing, we include them in the block scalar value.
|
||||
let i = nl + 1;
|
||||
ch = this.buffer[i];
|
||||
while (ch === ' ')
|
||||
ch = this.buffer[++i];
|
||||
if (ch === '\t') {
|
||||
while (ch === '\t' || ch === ' ' || ch === '\r' || ch === '\n')
|
||||
ch = this.buffer[++i];
|
||||
nl = i - 1;
|
||||
}
|
||||
else if (!this.blockScalarKeep) {
|
||||
do {
|
||||
let i = nl - 1;
|
||||
let ch = this.buffer[i];
|
||||
if (ch === '\r')
|
||||
ch = this.buffer[--i];
|
||||
const lastChar = i; // Drop the line if last char not more indented
|
||||
while (ch === ' ')
|
||||
ch = this.buffer[--i];
|
||||
if (ch === '\n' && i >= this.pos && i + 1 + indent > lastChar)
|
||||
nl = i;
|
||||
else
|
||||
break;
|
||||
} while (true);
|
||||
}
|
||||
yield cst.SCALAR;
|
||||
yield* this.pushToIndex(nl + 1, true);
|
||||
return yield* this.parseLineStart();
|
||||
}
|
||||
*parsePlainScalar() {
|
||||
const inFlow = this.flowLevel > 0;
|
||||
let end = this.pos - 1;
|
||||
let i = this.pos - 1;
|
||||
let ch;
|
||||
while ((ch = this.buffer[++i])) {
|
||||
if (ch === ':') {
|
||||
const next = this.buffer[i + 1];
|
||||
if (isEmpty(next) || (inFlow && flowIndicatorChars.has(next)))
|
||||
break;
|
||||
end = i;
|
||||
}
|
||||
else if (isEmpty(ch)) {
|
||||
let next = this.buffer[i + 1];
|
||||
if (ch === '\r') {
|
||||
if (next === '\n') {
|
||||
i += 1;
|
||||
ch = '\n';
|
||||
next = this.buffer[i + 1];
|
||||
}
|
||||
else
|
||||
end = i;
|
||||
}
|
||||
if (next === '#' || (inFlow && flowIndicatorChars.has(next)))
|
||||
break;
|
||||
if (ch === '\n') {
|
||||
const cs = this.continueScalar(i + 1);
|
||||
if (cs === -1)
|
||||
break;
|
||||
i = Math.max(i, cs - 2); // to advance, but still account for ' #'
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (inFlow && flowIndicatorChars.has(ch))
|
||||
break;
|
||||
end = i;
|
||||
}
|
||||
}
|
||||
if (!ch && !this.atEnd)
|
||||
return this.setNext('plain-scalar');
|
||||
yield cst.SCALAR;
|
||||
yield* this.pushToIndex(end + 1, true);
|
||||
return inFlow ? 'flow' : 'doc';
|
||||
}
|
||||
*pushCount(n) {
|
||||
if (n > 0) {
|
||||
yield this.buffer.substr(this.pos, n);
|
||||
this.pos += n;
|
||||
return n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
*pushToIndex(i, allowEmpty) {
|
||||
const s = this.buffer.slice(this.pos, i);
|
||||
if (s) {
|
||||
yield s;
|
||||
this.pos += s.length;
|
||||
return s.length;
|
||||
}
|
||||
else if (allowEmpty)
|
||||
yield '';
|
||||
return 0;
|
||||
}
|
||||
*pushIndicators() {
|
||||
let n = 0;
|
||||
loop: while (true) {
|
||||
switch (this.charAt(0)) {
|
||||
case '!':
|
||||
n += yield* this.pushTag();
|
||||
n += yield* this.pushSpaces(true);
|
||||
continue loop;
|
||||
case '&':
|
||||
n += yield* this.pushUntil(isNotAnchorChar);
|
||||
n += yield* this.pushSpaces(true);
|
||||
continue loop;
|
||||
case '-': // this is an error
|
||||
case '?': // this is an error outside flow collections
|
||||
case ':': {
|
||||
const inFlow = this.flowLevel > 0;
|
||||
const ch1 = this.charAt(1);
|
||||
if (isEmpty(ch1) || (inFlow && flowIndicatorChars.has(ch1))) {
|
||||
if (!inFlow)
|
||||
this.indentNext = this.indentValue + 1;
|
||||
else if (this.flowKey)
|
||||
this.flowKey = false;
|
||||
n += yield* this.pushCount(1);
|
||||
n += yield* this.pushSpaces(true);
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
break loop;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
*pushTag() {
|
||||
if (this.charAt(1) === '<') {
|
||||
let i = this.pos + 2;
|
||||
let ch = this.buffer[i];
|
||||
while (!isEmpty(ch) && ch !== '>')
|
||||
ch = this.buffer[++i];
|
||||
return yield* this.pushToIndex(ch === '>' ? i + 1 : i, false);
|
||||
}
|
||||
else {
|
||||
let i = this.pos + 1;
|
||||
let ch = this.buffer[i];
|
||||
while (ch) {
|
||||
if (tagChars.has(ch))
|
||||
ch = this.buffer[++i];
|
||||
else if (ch === '%' &&
|
||||
hexDigits.has(this.buffer[i + 1]) &&
|
||||
hexDigits.has(this.buffer[i + 2])) {
|
||||
ch = this.buffer[(i += 3)];
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
return yield* this.pushToIndex(i, false);
|
||||
}
|
||||
}
|
||||
*pushNewline() {
|
||||
const ch = this.buffer[this.pos];
|
||||
if (ch === '\n')
|
||||
return yield* this.pushCount(1);
|
||||
else if (ch === '\r' && this.charAt(1) === '\n')
|
||||
return yield* this.pushCount(2);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
*pushSpaces(allowTabs) {
|
||||
let i = this.pos - 1;
|
||||
let ch;
|
||||
do {
|
||||
ch = this.buffer[++i];
|
||||
} while (ch === ' ' || (allowTabs && ch === '\t'));
|
||||
const n = i - this.pos;
|
||||
if (n > 0) {
|
||||
yield this.buffer.substr(this.pos, n);
|
||||
this.pos = i;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
*pushUntil(test) {
|
||||
let i = this.pos;
|
||||
let ch = this.buffer[i];
|
||||
while (!test(ch))
|
||||
ch = this.buffer[++i];
|
||||
return yield* this.pushToIndex(i, false);
|
||||
}
|
||||
}
|
||||
|
||||
exports.Lexer = Lexer;
|
||||
41
bundled-addons/pithos/vendor/yaml/lib/parse/line-counter.js
vendored
Normal file
41
bundled-addons/pithos/vendor/yaml/lib/parse/line-counter.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Tracks newlines during parsing in order to provide an efficient API for
|
||||
* determining the one-indexed `{ line, col }` position for any offset
|
||||
* within the input.
|
||||
*/
|
||||
class LineCounter {
|
||||
constructor() {
|
||||
this.lineStarts = [];
|
||||
/**
|
||||
* Should be called in ascending order. Otherwise, call
|
||||
* `lineCounter.lineStarts.sort()` before calling `linePos()`.
|
||||
*/
|
||||
this.addNewLine = (offset) => this.lineStarts.push(offset);
|
||||
/**
|
||||
* Performs a binary search and returns the 1-indexed { line, col }
|
||||
* position of `offset`. If `line === 0`, `addNewLine` has never been
|
||||
* called or `offset` is before the first known newline.
|
||||
*/
|
||||
this.linePos = (offset) => {
|
||||
let low = 0;
|
||||
let high = this.lineStarts.length;
|
||||
while (low < high) {
|
||||
const mid = (low + high) >> 1; // Math.floor((low + high) / 2)
|
||||
if (this.lineStarts[mid] < offset)
|
||||
low = mid + 1;
|
||||
else
|
||||
high = mid;
|
||||
}
|
||||
if (this.lineStarts[low] === offset)
|
||||
return { line: low + 1, col: 1 };
|
||||
if (low === 0)
|
||||
return { line: 0, col: offset };
|
||||
const start = this.lineStarts[low - 1];
|
||||
return { line: low, col: offset - start + 1 };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.LineCounter = LineCounter;
|
||||
980
bundled-addons/pithos/vendor/yaml/lib/parse/parser.js
vendored
Normal file
980
bundled-addons/pithos/vendor/yaml/lib/parse/parser.js
vendored
Normal file
|
|
@ -0,0 +1,980 @@
|
|||
'use strict';
|
||||
|
||||
var node_process = require('process');
|
||||
var cst = require('./cst.js');
|
||||
var lexer = require('./lexer.js');
|
||||
|
||||
function includesToken(list, type) {
|
||||
for (let i = 0; i < list.length; ++i)
|
||||
if (list[i].type === type)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
function findNonEmptyIndex(list) {
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
switch (list[i].type) {
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
break;
|
||||
default:
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function isFlowToken(token) {
|
||||
switch (token?.type) {
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar':
|
||||
case 'flow-collection':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function getPrevProps(parent) {
|
||||
switch (parent.type) {
|
||||
case 'document':
|
||||
return parent.start;
|
||||
case 'block-map': {
|
||||
const it = parent.items[parent.items.length - 1];
|
||||
return it.sep ?? it.start;
|
||||
}
|
||||
case 'block-seq':
|
||||
return parent.items[parent.items.length - 1].start;
|
||||
/* istanbul ignore next should not happen */
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/** Note: May modify input array */
|
||||
function getFirstKeyStartProps(prev) {
|
||||
if (prev.length === 0)
|
||||
return [];
|
||||
let i = prev.length;
|
||||
loop: while (--i >= 0) {
|
||||
switch (prev[i].type) {
|
||||
case 'doc-start':
|
||||
case 'explicit-key-ind':
|
||||
case 'map-value-ind':
|
||||
case 'seq-item-ind':
|
||||
case 'newline':
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
while (prev[++i]?.type === 'space') {
|
||||
/* loop */
|
||||
}
|
||||
return prev.splice(i, prev.length);
|
||||
}
|
||||
function arrayPushArray(target, source) {
|
||||
// May exhaust call stack with large `source` array
|
||||
if (source.length < 1e5)
|
||||
Array.prototype.push.apply(target, source);
|
||||
else
|
||||
for (let i = 0; i < source.length; ++i)
|
||||
target.push(source[i]);
|
||||
}
|
||||
function fixFlowSeqItems(fc) {
|
||||
if (fc.start.type === 'flow-seq-start') {
|
||||
for (const it of fc.items) {
|
||||
if (it.sep &&
|
||||
!it.value &&
|
||||
!includesToken(it.start, 'explicit-key-ind') &&
|
||||
!includesToken(it.sep, 'map-value-ind')) {
|
||||
if (it.key)
|
||||
it.value = it.key;
|
||||
delete it.key;
|
||||
if (isFlowToken(it.value)) {
|
||||
if (it.value.end)
|
||||
arrayPushArray(it.value.end, it.sep);
|
||||
else
|
||||
it.value.end = it.sep;
|
||||
}
|
||||
else
|
||||
arrayPushArray(it.start, it.sep);
|
||||
delete it.sep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A YAML concrete syntax tree (CST) parser
|
||||
*
|
||||
* ```ts
|
||||
* const src: string = ...
|
||||
* for (const token of new Parser().parse(src)) {
|
||||
* // token: Token
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* To use the parser with a user-provided lexer:
|
||||
*
|
||||
* ```ts
|
||||
* function* parse(source: string, lexer: Lexer) {
|
||||
* const parser = new Parser()
|
||||
* for (const lexeme of lexer.lex(source))
|
||||
* yield* parser.next(lexeme)
|
||||
* yield* parser.end()
|
||||
* }
|
||||
*
|
||||
* const src: string = ...
|
||||
* const lexer = new Lexer()
|
||||
* for (const token of parse(src, lexer)) {
|
||||
* // token: Token
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class Parser {
|
||||
/**
|
||||
* @param onNewLine - If defined, called separately with the start position of
|
||||
* each new line (in `parse()`, including the start of input).
|
||||
*/
|
||||
constructor(onNewLine) {
|
||||
/** If true, space and sequence indicators count as indentation */
|
||||
this.atNewLine = true;
|
||||
/** If true, next token is a scalar value */
|
||||
this.atScalar = false;
|
||||
/** Current indentation level */
|
||||
this.indent = 0;
|
||||
/** Current offset since the start of parsing */
|
||||
this.offset = 0;
|
||||
/** On the same line with a block map key */
|
||||
this.onKeyLine = false;
|
||||
/** Top indicates the node that's currently being built */
|
||||
this.stack = [];
|
||||
/** The source of the current token, set in parse() */
|
||||
this.source = '';
|
||||
/** The type of the current token, set in parse() */
|
||||
this.type = '';
|
||||
// Must be defined after `next()`
|
||||
this.lexer = new lexer.Lexer();
|
||||
this.onNewLine = onNewLine;
|
||||
}
|
||||
/**
|
||||
* Parse `source` as a YAML stream.
|
||||
* If `incomplete`, a part of the last line may be left as a buffer for the next call.
|
||||
*
|
||||
* Errors are not thrown, but yielded as `{ type: 'error', message }` tokens.
|
||||
*
|
||||
* @returns A generator of tokens representing each directive, document, and other structure.
|
||||
*/
|
||||
*parse(source, incomplete = false) {
|
||||
if (this.onNewLine && this.offset === 0)
|
||||
this.onNewLine(0);
|
||||
for (const lexeme of this.lexer.lex(source, incomplete))
|
||||
yield* this.next(lexeme);
|
||||
if (!incomplete)
|
||||
yield* this.end();
|
||||
}
|
||||
/**
|
||||
* Advance the parser by the `source` of one lexical token.
|
||||
*/
|
||||
*next(source) {
|
||||
this.source = source;
|
||||
if (node_process.env.LOG_TOKENS)
|
||||
console.log('|', cst.prettyToken(source));
|
||||
if (this.atScalar) {
|
||||
this.atScalar = false;
|
||||
yield* this.step();
|
||||
this.offset += source.length;
|
||||
return;
|
||||
}
|
||||
const type = cst.tokenType(source);
|
||||
if (!type) {
|
||||
const message = `Not a YAML token: ${source}`;
|
||||
yield* this.pop({ type: 'error', offset: this.offset, message, source });
|
||||
this.offset += source.length;
|
||||
}
|
||||
else if (type === 'scalar') {
|
||||
this.atNewLine = false;
|
||||
this.atScalar = true;
|
||||
this.type = 'scalar';
|
||||
}
|
||||
else {
|
||||
this.type = type;
|
||||
yield* this.step();
|
||||
switch (type) {
|
||||
case 'newline':
|
||||
this.atNewLine = true;
|
||||
this.indent = 0;
|
||||
if (this.onNewLine)
|
||||
this.onNewLine(this.offset + source.length);
|
||||
break;
|
||||
case 'space':
|
||||
if (this.atNewLine && source[0] === ' ')
|
||||
this.indent += source.length;
|
||||
break;
|
||||
case 'explicit-key-ind':
|
||||
case 'map-value-ind':
|
||||
case 'seq-item-ind':
|
||||
if (this.atNewLine)
|
||||
this.indent += source.length;
|
||||
break;
|
||||
case 'doc-mode':
|
||||
case 'flow-error-end':
|
||||
return;
|
||||
default:
|
||||
this.atNewLine = false;
|
||||
}
|
||||
this.offset += source.length;
|
||||
}
|
||||
}
|
||||
/** Call at end of input to push out any remaining constructions */
|
||||
*end() {
|
||||
while (this.stack.length > 0)
|
||||
yield* this.pop();
|
||||
}
|
||||
get sourceToken() {
|
||||
const st = {
|
||||
type: this.type,
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
source: this.source
|
||||
};
|
||||
return st;
|
||||
}
|
||||
*step() {
|
||||
const top = this.peek(1);
|
||||
if (this.type === 'doc-end' && top?.type !== 'doc-end') {
|
||||
while (this.stack.length > 0)
|
||||
yield* this.pop();
|
||||
this.stack.push({
|
||||
type: 'doc-end',
|
||||
offset: this.offset,
|
||||
source: this.source
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!top)
|
||||
return yield* this.stream();
|
||||
switch (top.type) {
|
||||
case 'document':
|
||||
return yield* this.document(top);
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar':
|
||||
return yield* this.scalar(top);
|
||||
case 'block-scalar':
|
||||
return yield* this.blockScalar(top);
|
||||
case 'block-map':
|
||||
return yield* this.blockMap(top);
|
||||
case 'block-seq':
|
||||
return yield* this.blockSequence(top);
|
||||
case 'flow-collection':
|
||||
return yield* this.flowCollection(top);
|
||||
case 'doc-end':
|
||||
return yield* this.documentEnd(top);
|
||||
}
|
||||
/* istanbul ignore next should not happen */
|
||||
yield* this.pop();
|
||||
}
|
||||
peek(n) {
|
||||
return this.stack[this.stack.length - n];
|
||||
}
|
||||
*pop(error) {
|
||||
const token = error ?? this.stack.pop();
|
||||
/* istanbul ignore if should not happen */
|
||||
if (!token) {
|
||||
const message = 'Tried to pop an empty stack';
|
||||
yield { type: 'error', offset: this.offset, source: '', message };
|
||||
}
|
||||
else if (this.stack.length === 0) {
|
||||
yield token;
|
||||
}
|
||||
else {
|
||||
const top = this.peek(1);
|
||||
if (token.type === 'block-scalar') {
|
||||
// Block scalars use their parent rather than header indent
|
||||
token.indent = 'indent' in top ? top.indent : 0;
|
||||
}
|
||||
else if (token.type === 'flow-collection' && top.type === 'document') {
|
||||
// Ignore all indent for top-level flow collections
|
||||
token.indent = 0;
|
||||
}
|
||||
if (token.type === 'flow-collection')
|
||||
fixFlowSeqItems(token);
|
||||
switch (top.type) {
|
||||
case 'document':
|
||||
top.value = token;
|
||||
break;
|
||||
case 'block-scalar':
|
||||
top.props.push(token); // error
|
||||
break;
|
||||
case 'block-map': {
|
||||
const it = top.items[top.items.length - 1];
|
||||
if (it.value) {
|
||||
top.items.push({ start: [], key: token, sep: [] });
|
||||
this.onKeyLine = true;
|
||||
return;
|
||||
}
|
||||
else if (it.sep) {
|
||||
it.value = token;
|
||||
}
|
||||
else {
|
||||
Object.assign(it, { key: token, sep: [] });
|
||||
this.onKeyLine = !it.explicitKey;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'block-seq': {
|
||||
const it = top.items[top.items.length - 1];
|
||||
if (it.value)
|
||||
top.items.push({ start: [], value: token });
|
||||
else
|
||||
it.value = token;
|
||||
break;
|
||||
}
|
||||
case 'flow-collection': {
|
||||
const it = top.items[top.items.length - 1];
|
||||
if (!it || it.value)
|
||||
top.items.push({ start: [], key: token, sep: [] });
|
||||
else if (it.sep)
|
||||
it.value = token;
|
||||
else
|
||||
Object.assign(it, { key: token, sep: [] });
|
||||
return;
|
||||
}
|
||||
/* istanbul ignore next should not happen */
|
||||
default:
|
||||
yield* this.pop();
|
||||
yield* this.pop(token);
|
||||
}
|
||||
if ((top.type === 'document' ||
|
||||
top.type === 'block-map' ||
|
||||
top.type === 'block-seq') &&
|
||||
(token.type === 'block-map' || token.type === 'block-seq')) {
|
||||
const last = token.items[token.items.length - 1];
|
||||
if (last &&
|
||||
!last.sep &&
|
||||
!last.value &&
|
||||
last.start.length > 0 &&
|
||||
findNonEmptyIndex(last.start) === -1 &&
|
||||
(token.indent === 0 ||
|
||||
last.start.every(st => st.type !== 'comment' || st.indent < token.indent))) {
|
||||
if (top.type === 'document')
|
||||
top.end = last.start;
|
||||
else
|
||||
top.items.push({ start: last.start });
|
||||
token.items.splice(-1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*stream() {
|
||||
switch (this.type) {
|
||||
case 'directive-line':
|
||||
yield { type: 'directive', offset: this.offset, source: this.source };
|
||||
return;
|
||||
case 'byte-order-mark':
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
yield this.sourceToken;
|
||||
return;
|
||||
case 'doc-mode':
|
||||
case 'doc-start': {
|
||||
const doc = {
|
||||
type: 'document',
|
||||
offset: this.offset,
|
||||
start: []
|
||||
};
|
||||
if (this.type === 'doc-start')
|
||||
doc.start.push(this.sourceToken);
|
||||
this.stack.push(doc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: 'error',
|
||||
offset: this.offset,
|
||||
message: `Unexpected ${this.type} token in YAML stream`,
|
||||
source: this.source
|
||||
};
|
||||
}
|
||||
*document(doc) {
|
||||
if (doc.value)
|
||||
return yield* this.lineEnd(doc);
|
||||
switch (this.type) {
|
||||
case 'doc-start': {
|
||||
if (findNonEmptyIndex(doc.start) !== -1) {
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
else
|
||||
doc.start.push(this.sourceToken);
|
||||
return;
|
||||
}
|
||||
case 'anchor':
|
||||
case 'tag':
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
doc.start.push(this.sourceToken);
|
||||
return;
|
||||
}
|
||||
const bv = this.startBlockValue(doc);
|
||||
if (bv)
|
||||
this.stack.push(bv);
|
||||
else {
|
||||
yield {
|
||||
type: 'error',
|
||||
offset: this.offset,
|
||||
message: `Unexpected ${this.type} token in YAML document`,
|
||||
source: this.source
|
||||
};
|
||||
}
|
||||
}
|
||||
*scalar(scalar) {
|
||||
if (this.type === 'map-value-ind') {
|
||||
const prev = getPrevProps(this.peek(2));
|
||||
const start = getFirstKeyStartProps(prev);
|
||||
let sep;
|
||||
if (scalar.end) {
|
||||
sep = scalar.end;
|
||||
sep.push(this.sourceToken);
|
||||
delete scalar.end;
|
||||
}
|
||||
else
|
||||
sep = [this.sourceToken];
|
||||
const map = {
|
||||
type: 'block-map',
|
||||
offset: scalar.offset,
|
||||
indent: scalar.indent,
|
||||
items: [{ start, key: scalar, sep }]
|
||||
};
|
||||
this.onKeyLine = true;
|
||||
this.stack[this.stack.length - 1] = map;
|
||||
}
|
||||
else
|
||||
yield* this.lineEnd(scalar);
|
||||
}
|
||||
*blockScalar(scalar) {
|
||||
switch (this.type) {
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
scalar.props.push(this.sourceToken);
|
||||
return;
|
||||
case 'scalar':
|
||||
scalar.source = this.source;
|
||||
// block-scalar source includes trailing newline
|
||||
this.atNewLine = true;
|
||||
this.indent = 0;
|
||||
if (this.onNewLine) {
|
||||
let nl = this.source.indexOf('\n') + 1;
|
||||
while (nl !== 0) {
|
||||
this.onNewLine(this.offset + nl);
|
||||
nl = this.source.indexOf('\n', nl) + 1;
|
||||
}
|
||||
}
|
||||
yield* this.pop();
|
||||
break;
|
||||
/* istanbul ignore next should not happen */
|
||||
default:
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
}
|
||||
*blockMap(map) {
|
||||
const it = map.items[map.items.length - 1];
|
||||
// it.sep is true-ish if pair already has key or : separator
|
||||
switch (this.type) {
|
||||
case 'newline':
|
||||
this.onKeyLine = false;
|
||||
if (it.value) {
|
||||
const end = 'end' in it.value ? it.value.end : undefined;
|
||||
const last = Array.isArray(end) ? end[end.length - 1] : undefined;
|
||||
if (last?.type === 'comment')
|
||||
end?.push(this.sourceToken);
|
||||
else
|
||||
map.items.push({ start: [this.sourceToken] });
|
||||
}
|
||||
else if (it.sep) {
|
||||
it.sep.push(this.sourceToken);
|
||||
}
|
||||
else {
|
||||
it.start.push(this.sourceToken);
|
||||
}
|
||||
return;
|
||||
case 'space':
|
||||
case 'comment':
|
||||
if (it.value) {
|
||||
map.items.push({ start: [this.sourceToken] });
|
||||
}
|
||||
else if (it.sep) {
|
||||
it.sep.push(this.sourceToken);
|
||||
}
|
||||
else {
|
||||
if (this.atIndentedComment(it.start, map.indent)) {
|
||||
const prev = map.items[map.items.length - 2];
|
||||
const end = prev?.value?.end;
|
||||
if (Array.isArray(end)) {
|
||||
arrayPushArray(end, it.start);
|
||||
end.push(this.sourceToken);
|
||||
map.items.pop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
it.start.push(this.sourceToken);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.indent >= map.indent) {
|
||||
const atMapIndent = !this.onKeyLine && this.indent === map.indent;
|
||||
const atNextItem = atMapIndent &&
|
||||
(it.sep || it.explicitKey) &&
|
||||
this.type !== 'seq-item-ind';
|
||||
// For empty nodes, assign newline-separated not indented empty tokens to following node
|
||||
let start = [];
|
||||
if (atNextItem && it.sep && !it.value) {
|
||||
const nl = [];
|
||||
for (let i = 0; i < it.sep.length; ++i) {
|
||||
const st = it.sep[i];
|
||||
switch (st.type) {
|
||||
case 'newline':
|
||||
nl.push(i);
|
||||
break;
|
||||
case 'space':
|
||||
break;
|
||||
case 'comment':
|
||||
if (st.indent > map.indent)
|
||||
nl.length = 0;
|
||||
break;
|
||||
default:
|
||||
nl.length = 0;
|
||||
}
|
||||
}
|
||||
if (nl.length >= 2)
|
||||
start = it.sep.splice(nl[1]);
|
||||
}
|
||||
switch (this.type) {
|
||||
case 'anchor':
|
||||
case 'tag':
|
||||
if (atNextItem || it.value) {
|
||||
start.push(this.sourceToken);
|
||||
map.items.push({ start });
|
||||
this.onKeyLine = true;
|
||||
}
|
||||
else if (it.sep) {
|
||||
it.sep.push(this.sourceToken);
|
||||
}
|
||||
else {
|
||||
it.start.push(this.sourceToken);
|
||||
}
|
||||
return;
|
||||
case 'explicit-key-ind':
|
||||
if (!it.sep && !it.explicitKey) {
|
||||
it.start.push(this.sourceToken);
|
||||
it.explicitKey = true;
|
||||
}
|
||||
else if (atNextItem || it.value) {
|
||||
start.push(this.sourceToken);
|
||||
map.items.push({ start, explicitKey: true });
|
||||
}
|
||||
else {
|
||||
this.stack.push({
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start: [this.sourceToken], explicitKey: true }]
|
||||
});
|
||||
}
|
||||
this.onKeyLine = true;
|
||||
return;
|
||||
case 'map-value-ind':
|
||||
if (it.explicitKey) {
|
||||
if (!it.sep) {
|
||||
if (includesToken(it.start, 'newline')) {
|
||||
Object.assign(it, { key: null, sep: [this.sourceToken] });
|
||||
}
|
||||
else {
|
||||
const start = getFirstKeyStartProps(it.start);
|
||||
this.stack.push({
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start, key: null, sep: [this.sourceToken] }]
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (it.value) {
|
||||
map.items.push({ start: [], key: null, sep: [this.sourceToken] });
|
||||
}
|
||||
else if (includesToken(it.sep, 'map-value-ind')) {
|
||||
this.stack.push({
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start, key: null, sep: [this.sourceToken] }]
|
||||
});
|
||||
}
|
||||
else if (isFlowToken(it.key) &&
|
||||
!includesToken(it.sep, 'newline')) {
|
||||
const start = getFirstKeyStartProps(it.start);
|
||||
const key = it.key;
|
||||
const sep = it.sep;
|
||||
sep.push(this.sourceToken);
|
||||
// @ts-expect-error type guard is wrong here
|
||||
delete it.key;
|
||||
// @ts-expect-error type guard is wrong here
|
||||
delete it.sep;
|
||||
this.stack.push({
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start, key, sep }]
|
||||
});
|
||||
}
|
||||
else if (start.length > 0) {
|
||||
// Not actually at next item
|
||||
it.sep = it.sep.concat(start, this.sourceToken);
|
||||
}
|
||||
else {
|
||||
it.sep.push(this.sourceToken);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!it.sep) {
|
||||
Object.assign(it, { key: null, sep: [this.sourceToken] });
|
||||
}
|
||||
else if (it.value || atNextItem) {
|
||||
map.items.push({ start, key: null, sep: [this.sourceToken] });
|
||||
}
|
||||
else if (includesToken(it.sep, 'map-value-ind')) {
|
||||
this.stack.push({
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start: [], key: null, sep: [this.sourceToken] }]
|
||||
});
|
||||
}
|
||||
else {
|
||||
it.sep.push(this.sourceToken);
|
||||
}
|
||||
}
|
||||
this.onKeyLine = true;
|
||||
return;
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar': {
|
||||
const fs = this.flowScalar(this.type);
|
||||
if (atNextItem || it.value) {
|
||||
map.items.push({ start, key: fs, sep: [] });
|
||||
this.onKeyLine = true;
|
||||
}
|
||||
else if (it.sep) {
|
||||
this.stack.push(fs);
|
||||
}
|
||||
else {
|
||||
Object.assign(it, { key: fs, sep: [] });
|
||||
this.onKeyLine = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
const bv = this.startBlockValue(map);
|
||||
if (bv) {
|
||||
if (bv.type === 'block-seq') {
|
||||
if (!it.explicitKey &&
|
||||
it.sep &&
|
||||
!includesToken(it.sep, 'newline')) {
|
||||
yield* this.pop({
|
||||
type: 'error',
|
||||
offset: this.offset,
|
||||
message: 'Unexpected block-seq-ind on same line with key',
|
||||
source: this.source
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (atMapIndent) {
|
||||
map.items.push({ start });
|
||||
}
|
||||
this.stack.push(bv);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
*blockSequence(seq) {
|
||||
const it = seq.items[seq.items.length - 1];
|
||||
switch (this.type) {
|
||||
case 'newline':
|
||||
if (it.value) {
|
||||
const end = 'end' in it.value ? it.value.end : undefined;
|
||||
const last = Array.isArray(end) ? end[end.length - 1] : undefined;
|
||||
if (last?.type === 'comment')
|
||||
end?.push(this.sourceToken);
|
||||
else
|
||||
seq.items.push({ start: [this.sourceToken] });
|
||||
}
|
||||
else
|
||||
it.start.push(this.sourceToken);
|
||||
return;
|
||||
case 'space':
|
||||
case 'comment':
|
||||
if (it.value)
|
||||
seq.items.push({ start: [this.sourceToken] });
|
||||
else {
|
||||
if (this.atIndentedComment(it.start, seq.indent)) {
|
||||
const prev = seq.items[seq.items.length - 2];
|
||||
const end = prev?.value?.end;
|
||||
if (Array.isArray(end)) {
|
||||
arrayPushArray(end, it.start);
|
||||
end.push(this.sourceToken);
|
||||
seq.items.pop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
it.start.push(this.sourceToken);
|
||||
}
|
||||
return;
|
||||
case 'anchor':
|
||||
case 'tag':
|
||||
if (it.value || this.indent <= seq.indent)
|
||||
break;
|
||||
it.start.push(this.sourceToken);
|
||||
return;
|
||||
case 'seq-item-ind':
|
||||
if (this.indent !== seq.indent)
|
||||
break;
|
||||
if (it.value || includesToken(it.start, 'seq-item-ind'))
|
||||
seq.items.push({ start: [this.sourceToken] });
|
||||
else
|
||||
it.start.push(this.sourceToken);
|
||||
return;
|
||||
}
|
||||
if (this.indent > seq.indent) {
|
||||
const bv = this.startBlockValue(seq);
|
||||
if (bv) {
|
||||
this.stack.push(bv);
|
||||
return;
|
||||
}
|
||||
}
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
*flowCollection(fc) {
|
||||
const it = fc.items[fc.items.length - 1];
|
||||
if (this.type === 'flow-error-end') {
|
||||
let top;
|
||||
do {
|
||||
yield* this.pop();
|
||||
top = this.peek(1);
|
||||
} while (top?.type === 'flow-collection');
|
||||
}
|
||||
else if (fc.end.length === 0) {
|
||||
switch (this.type) {
|
||||
case 'comma':
|
||||
case 'explicit-key-ind':
|
||||
if (!it || it.sep)
|
||||
fc.items.push({ start: [this.sourceToken] });
|
||||
else
|
||||
it.start.push(this.sourceToken);
|
||||
return;
|
||||
case 'map-value-ind':
|
||||
if (!it || it.value)
|
||||
fc.items.push({ start: [], key: null, sep: [this.sourceToken] });
|
||||
else if (it.sep)
|
||||
it.sep.push(this.sourceToken);
|
||||
else
|
||||
Object.assign(it, { key: null, sep: [this.sourceToken] });
|
||||
return;
|
||||
case 'space':
|
||||
case 'comment':
|
||||
case 'newline':
|
||||
case 'anchor':
|
||||
case 'tag':
|
||||
if (!it || it.value)
|
||||
fc.items.push({ start: [this.sourceToken] });
|
||||
else if (it.sep)
|
||||
it.sep.push(this.sourceToken);
|
||||
else
|
||||
it.start.push(this.sourceToken);
|
||||
return;
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar': {
|
||||
const fs = this.flowScalar(this.type);
|
||||
if (!it || it.value)
|
||||
fc.items.push({ start: [], key: fs, sep: [] });
|
||||
else if (it.sep)
|
||||
this.stack.push(fs);
|
||||
else
|
||||
Object.assign(it, { key: fs, sep: [] });
|
||||
return;
|
||||
}
|
||||
case 'flow-map-end':
|
||||
case 'flow-seq-end':
|
||||
fc.end.push(this.sourceToken);
|
||||
return;
|
||||
}
|
||||
const bv = this.startBlockValue(fc);
|
||||
/* istanbul ignore else should not happen */
|
||||
if (bv)
|
||||
this.stack.push(bv);
|
||||
else {
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
}
|
||||
else {
|
||||
const parent = this.peek(2);
|
||||
if (parent.type === 'block-map' &&
|
||||
((this.type === 'map-value-ind' && parent.indent === fc.indent) ||
|
||||
(this.type === 'newline' &&
|
||||
!parent.items[parent.items.length - 1].sep))) {
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
}
|
||||
else if (this.type === 'map-value-ind' &&
|
||||
parent.type !== 'flow-collection') {
|
||||
const prev = getPrevProps(parent);
|
||||
const start = getFirstKeyStartProps(prev);
|
||||
fixFlowSeqItems(fc);
|
||||
const sep = fc.end.splice(1, fc.end.length);
|
||||
sep.push(this.sourceToken);
|
||||
const map = {
|
||||
type: 'block-map',
|
||||
offset: fc.offset,
|
||||
indent: fc.indent,
|
||||
items: [{ start, key: fc, sep }]
|
||||
};
|
||||
this.onKeyLine = true;
|
||||
this.stack[this.stack.length - 1] = map;
|
||||
}
|
||||
else {
|
||||
yield* this.lineEnd(fc);
|
||||
}
|
||||
}
|
||||
}
|
||||
flowScalar(type) {
|
||||
if (this.onNewLine) {
|
||||
let nl = this.source.indexOf('\n') + 1;
|
||||
while (nl !== 0) {
|
||||
this.onNewLine(this.offset + nl);
|
||||
nl = this.source.indexOf('\n', nl) + 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type,
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
source: this.source
|
||||
};
|
||||
}
|
||||
startBlockValue(parent) {
|
||||
switch (this.type) {
|
||||
case 'alias':
|
||||
case 'scalar':
|
||||
case 'single-quoted-scalar':
|
||||
case 'double-quoted-scalar':
|
||||
return this.flowScalar(this.type);
|
||||
case 'block-scalar-header':
|
||||
return {
|
||||
type: 'block-scalar',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
props: [this.sourceToken],
|
||||
source: ''
|
||||
};
|
||||
case 'flow-map-start':
|
||||
case 'flow-seq-start':
|
||||
return {
|
||||
type: 'flow-collection',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
start: this.sourceToken,
|
||||
items: [],
|
||||
end: []
|
||||
};
|
||||
case 'seq-item-ind':
|
||||
return {
|
||||
type: 'block-seq',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start: [this.sourceToken] }]
|
||||
};
|
||||
case 'explicit-key-ind': {
|
||||
this.onKeyLine = true;
|
||||
const prev = getPrevProps(parent);
|
||||
const start = getFirstKeyStartProps(prev);
|
||||
start.push(this.sourceToken);
|
||||
return {
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start, explicitKey: true }]
|
||||
};
|
||||
}
|
||||
case 'map-value-ind': {
|
||||
this.onKeyLine = true;
|
||||
const prev = getPrevProps(parent);
|
||||
const start = getFirstKeyStartProps(prev);
|
||||
return {
|
||||
type: 'block-map',
|
||||
offset: this.offset,
|
||||
indent: this.indent,
|
||||
items: [{ start, key: null, sep: [this.sourceToken] }]
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
atIndentedComment(start, indent) {
|
||||
if (this.type !== 'comment')
|
||||
return false;
|
||||
if (this.indent <= indent)
|
||||
return false;
|
||||
return start.every(st => st.type === 'newline' || st.type === 'space');
|
||||
}
|
||||
*documentEnd(docEnd) {
|
||||
if (this.type !== 'doc-mode') {
|
||||
if (docEnd.end)
|
||||
docEnd.end.push(this.sourceToken);
|
||||
else
|
||||
docEnd.end = [this.sourceToken];
|
||||
if (this.type === 'newline')
|
||||
yield* this.pop();
|
||||
}
|
||||
}
|
||||
*lineEnd(token) {
|
||||
switch (this.type) {
|
||||
case 'comma':
|
||||
case 'doc-start':
|
||||
case 'doc-end':
|
||||
case 'flow-seq-end':
|
||||
case 'flow-map-end':
|
||||
case 'map-value-ind':
|
||||
yield* this.pop();
|
||||
yield* this.step();
|
||||
break;
|
||||
case 'newline':
|
||||
this.onKeyLine = false;
|
||||
// fallthrough
|
||||
case 'space':
|
||||
case 'comment':
|
||||
default:
|
||||
// all other values are errors
|
||||
if (token.end)
|
||||
token.end.push(this.sourceToken);
|
||||
else
|
||||
token.end = [this.sourceToken];
|
||||
if (this.type === 'newline')
|
||||
yield* this.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.Parser = Parser;
|
||||
107
bundled-addons/pithos/vendor/yaml/lib/public-api.js
vendored
Normal file
107
bundled-addons/pithos/vendor/yaml/lib/public-api.js
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
'use strict';
|
||||
|
||||
var composer = require('./compose/composer.js');
|
||||
var Document = require('./doc/Document.js');
|
||||
var errors = require('./errors.js');
|
||||
var log = require('./log.js');
|
||||
var identity = require('./nodes/identity.js');
|
||||
var lineCounter = require('./parse/line-counter.js');
|
||||
var parser = require('./parse/parser.js');
|
||||
|
||||
function parseOptions(options) {
|
||||
const prettyErrors = options.prettyErrors !== false;
|
||||
const lineCounter$1 = options.lineCounter || (prettyErrors && new lineCounter.LineCounter()) || null;
|
||||
return { lineCounter: lineCounter$1, prettyErrors };
|
||||
}
|
||||
/**
|
||||
* Parse the input as a stream of YAML documents.
|
||||
*
|
||||
* Documents should be separated from each other by `...` or `---` marker lines.
|
||||
*
|
||||
* @returns If an empty `docs` array is returned, it will be of type
|
||||
* EmptyStream and contain additional stream information. In
|
||||
* TypeScript, you should use `'empty' in docs` as a type guard for it.
|
||||
*/
|
||||
function parseAllDocuments(source, options = {}) {
|
||||
const { lineCounter, prettyErrors } = parseOptions(options);
|
||||
const parser$1 = new parser.Parser(lineCounter?.addNewLine);
|
||||
const composer$1 = new composer.Composer(options);
|
||||
const docs = Array.from(composer$1.compose(parser$1.parse(source)));
|
||||
if (prettyErrors && lineCounter)
|
||||
for (const doc of docs) {
|
||||
doc.errors.forEach(errors.prettifyError(source, lineCounter));
|
||||
doc.warnings.forEach(errors.prettifyError(source, lineCounter));
|
||||
}
|
||||
if (docs.length > 0)
|
||||
return docs;
|
||||
return Object.assign([], { empty: true }, composer$1.streamInfo());
|
||||
}
|
||||
/** Parse an input string into a single YAML.Document */
|
||||
function parseDocument(source, options = {}) {
|
||||
const { lineCounter, prettyErrors } = parseOptions(options);
|
||||
const parser$1 = new parser.Parser(lineCounter?.addNewLine);
|
||||
const composer$1 = new composer.Composer(options);
|
||||
// `doc` is always set by compose.end(true) at the very latest
|
||||
let doc = null;
|
||||
for (const _doc of composer$1.compose(parser$1.parse(source), true, source.length)) {
|
||||
if (!doc)
|
||||
doc = _doc;
|
||||
else if (doc.options.logLevel !== 'silent') {
|
||||
doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), 'MULTIPLE_DOCS', 'Source contains multiple documents; please use YAML.parseAllDocuments()'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prettyErrors && lineCounter) {
|
||||
doc.errors.forEach(errors.prettifyError(source, lineCounter));
|
||||
doc.warnings.forEach(errors.prettifyError(source, lineCounter));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
function parse(src, reviver, options) {
|
||||
let _reviver = undefined;
|
||||
if (typeof reviver === 'function') {
|
||||
_reviver = reviver;
|
||||
}
|
||||
else if (options === undefined && reviver && typeof reviver === 'object') {
|
||||
options = reviver;
|
||||
}
|
||||
const doc = parseDocument(src, options);
|
||||
if (!doc)
|
||||
return null;
|
||||
doc.warnings.forEach(warning => log.warn(doc.options.logLevel, warning));
|
||||
if (doc.errors.length > 0) {
|
||||
if (doc.options.logLevel !== 'silent')
|
||||
throw doc.errors[0];
|
||||
else
|
||||
doc.errors = [];
|
||||
}
|
||||
return doc.toJS(Object.assign({ reviver: _reviver }, options));
|
||||
}
|
||||
function stringify(value, replacer, options) {
|
||||
let _replacer = null;
|
||||
if (typeof replacer === 'function' || Array.isArray(replacer)) {
|
||||
_replacer = replacer;
|
||||
}
|
||||
else if (options === undefined && replacer) {
|
||||
options = replacer;
|
||||
}
|
||||
if (typeof options === 'string')
|
||||
options = options.length;
|
||||
if (typeof options === 'number') {
|
||||
const indent = Math.round(options);
|
||||
options = indent < 1 ? undefined : indent > 8 ? { indent: 8 } : { indent };
|
||||
}
|
||||
if (value === undefined) {
|
||||
const { keepUndefined } = options ?? replacer ?? {};
|
||||
if (!keepUndefined)
|
||||
return undefined;
|
||||
}
|
||||
if (identity.isDocument(value) && !_replacer)
|
||||
return value.toString(options);
|
||||
return new Document.Document(value, _replacer, options).toString(options);
|
||||
}
|
||||
|
||||
exports.parse = parse;
|
||||
exports.parseAllDocuments = parseAllDocuments;
|
||||
exports.parseDocument = parseDocument;
|
||||
exports.stringify = stringify;
|
||||
39
bundled-addons/pithos/vendor/yaml/lib/schema/Schema.js
vendored
Normal file
39
bundled-addons/pithos/vendor/yaml/lib/schema/Schema.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var map = require('./common/map.js');
|
||||
var seq = require('./common/seq.js');
|
||||
var string = require('./common/string.js');
|
||||
var tags = require('./tags.js');
|
||||
|
||||
const sortMapEntriesByKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
||||
class Schema {
|
||||
constructor({ compat, customTags, merge, resolveKnownTags, schema, sortMapEntries, toStringDefaults }) {
|
||||
this.compat = Array.isArray(compat)
|
||||
? tags.getTags(compat, 'compat')
|
||||
: compat
|
||||
? tags.getTags(null, compat)
|
||||
: null;
|
||||
this.name = (typeof schema === 'string' && schema) || 'core';
|
||||
this.knownTags = resolveKnownTags ? tags.coreKnownTags : {};
|
||||
this.tags = tags.getTags(customTags, this.name, merge);
|
||||
this.toStringOptions = toStringDefaults ?? null;
|
||||
Object.defineProperty(this, identity.MAP, { value: map.map });
|
||||
Object.defineProperty(this, identity.SCALAR, { value: string.string });
|
||||
Object.defineProperty(this, identity.SEQ, { value: seq.seq });
|
||||
// Used by createMap()
|
||||
this.sortMapEntries =
|
||||
typeof sortMapEntries === 'function'
|
||||
? sortMapEntries
|
||||
: sortMapEntries === true
|
||||
? sortMapEntriesByKey
|
||||
: null;
|
||||
}
|
||||
clone() {
|
||||
const copy = Object.create(Schema.prototype, Object.getOwnPropertyDescriptors(this));
|
||||
copy.tags = this.tags.slice();
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Schema = Schema;
|
||||
19
bundled-addons/pithos/vendor/yaml/lib/schema/common/map.js
vendored
Normal file
19
bundled-addons/pithos/vendor/yaml/lib/schema/common/map.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var YAMLMap = require('../../nodes/YAMLMap.js');
|
||||
|
||||
const map = {
|
||||
collection: 'map',
|
||||
default: true,
|
||||
nodeClass: YAMLMap.YAMLMap,
|
||||
tag: 'tag:yaml.org,2002:map',
|
||||
resolve(map, onError) {
|
||||
if (!identity.isMap(map))
|
||||
onError('Expected a mapping for this tag');
|
||||
return map;
|
||||
},
|
||||
createNode: (schema, obj, ctx) => YAMLMap.YAMLMap.from(schema, obj, ctx)
|
||||
};
|
||||
|
||||
exports.map = map;
|
||||
17
bundled-addons/pithos/vendor/yaml/lib/schema/common/null.js
vendored
Normal file
17
bundled-addons/pithos/vendor/yaml/lib/schema/common/null.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
|
||||
const nullTag = {
|
||||
identify: value => value == null,
|
||||
createNode: () => new Scalar.Scalar(null),
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:null',
|
||||
test: /^(?:~|[Nn]ull|NULL)?$/,
|
||||
resolve: () => new Scalar.Scalar(null),
|
||||
stringify: ({ source }, ctx) => typeof source === 'string' && nullTag.test.test(source)
|
||||
? source
|
||||
: ctx.options.nullStr
|
||||
};
|
||||
|
||||
exports.nullTag = nullTag;
|
||||
19
bundled-addons/pithos/vendor/yaml/lib/schema/common/seq.js
vendored
Normal file
19
bundled-addons/pithos/vendor/yaml/lib/schema/common/seq.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var YAMLSeq = require('../../nodes/YAMLSeq.js');
|
||||
|
||||
const seq = {
|
||||
collection: 'seq',
|
||||
default: true,
|
||||
nodeClass: YAMLSeq.YAMLSeq,
|
||||
tag: 'tag:yaml.org,2002:seq',
|
||||
resolve(seq, onError) {
|
||||
if (!identity.isSeq(seq))
|
||||
onError('Expected a sequence for this tag');
|
||||
return seq;
|
||||
},
|
||||
createNode: (schema, obj, ctx) => YAMLSeq.YAMLSeq.from(schema, obj, ctx)
|
||||
};
|
||||
|
||||
exports.seq = seq;
|
||||
16
bundled-addons/pithos/vendor/yaml/lib/schema/common/string.js
vendored
Normal file
16
bundled-addons/pithos/vendor/yaml/lib/schema/common/string.js
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
var stringifyString = require('../../stringify/stringifyString.js');
|
||||
|
||||
const string = {
|
||||
identify: value => typeof value === 'string',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:str',
|
||||
resolve: str => str,
|
||||
stringify(item, ctx, onComment, onChompKeep) {
|
||||
ctx = Object.assign({ actualString: true }, ctx);
|
||||
return stringifyString.stringifyString(item, ctx, onComment, onChompKeep);
|
||||
}
|
||||
};
|
||||
|
||||
exports.string = string;
|
||||
21
bundled-addons/pithos/vendor/yaml/lib/schema/core/bool.js
vendored
Normal file
21
bundled-addons/pithos/vendor/yaml/lib/schema/core/bool.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
|
||||
const boolTag = {
|
||||
identify: value => typeof value === 'boolean',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:bool',
|
||||
test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/,
|
||||
resolve: str => new Scalar.Scalar(str[0] === 't' || str[0] === 'T'),
|
||||
stringify({ source, value }, ctx) {
|
||||
if (source && boolTag.test.test(source)) {
|
||||
const sv = source[0] === 't' || source[0] === 'T';
|
||||
if (value === sv)
|
||||
return source;
|
||||
}
|
||||
return value ? ctx.options.trueStr : ctx.options.falseStr;
|
||||
}
|
||||
};
|
||||
|
||||
exports.boolTag = boolTag;
|
||||
47
bundled-addons/pithos/vendor/yaml/lib/schema/core/float.js
vendored
Normal file
47
bundled-addons/pithos/vendor/yaml/lib/schema/core/float.js
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
var stringifyNumber = require('../../stringify/stringifyNumber.js');
|
||||
|
||||
const floatNaN = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,
|
||||
resolve: str => str.slice(-3).toLowerCase() === 'nan'
|
||||
? NaN
|
||||
: str[0] === '-'
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: Number.POSITIVE_INFINITY,
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
const floatExp = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
format: 'EXP',
|
||||
test: /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)[eE][-+]?[0-9]+$/,
|
||||
resolve: str => parseFloat(str),
|
||||
stringify(node) {
|
||||
const num = Number(node.value);
|
||||
return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node);
|
||||
}
|
||||
};
|
||||
const float = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
test: /^[-+]?(?:\.[0-9]+|[0-9]+\.[0-9]*)$/,
|
||||
resolve(str) {
|
||||
const node = new Scalar.Scalar(parseFloat(str));
|
||||
const dot = str.indexOf('.');
|
||||
if (dot !== -1 && str[str.length - 1] === '0')
|
||||
node.minFractionDigits = str.length - dot - 1;
|
||||
return node;
|
||||
},
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
|
||||
exports.float = float;
|
||||
exports.floatExp = floatExp;
|
||||
exports.floatNaN = floatNaN;
|
||||
42
bundled-addons/pithos/vendor/yaml/lib/schema/core/int.js
vendored
Normal file
42
bundled-addons/pithos/vendor/yaml/lib/schema/core/int.js
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
var stringifyNumber = require('../../stringify/stringifyNumber.js');
|
||||
|
||||
const intIdentify = (value) => typeof value === 'bigint' || Number.isInteger(value);
|
||||
const intResolve = (str, offset, radix, { intAsBigInt }) => (intAsBigInt ? BigInt(str) : parseInt(str.substring(offset), radix));
|
||||
function intStringify(node, radix, prefix) {
|
||||
const { value } = node;
|
||||
if (intIdentify(value) && value >= 0)
|
||||
return prefix + value.toString(radix);
|
||||
return stringifyNumber.stringifyNumber(node);
|
||||
}
|
||||
const intOct = {
|
||||
identify: value => intIdentify(value) && value >= 0,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'OCT',
|
||||
test: /^0o[0-7]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 2, 8, opt),
|
||||
stringify: node => intStringify(node, 8, '0o')
|
||||
};
|
||||
const int = {
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
test: /^[-+]?[0-9]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt),
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
const intHex = {
|
||||
identify: value => intIdentify(value) && value >= 0,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'HEX',
|
||||
test: /^0x[0-9a-fA-F]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt),
|
||||
stringify: node => intStringify(node, 16, '0x')
|
||||
};
|
||||
|
||||
exports.int = int;
|
||||
exports.intHex = intHex;
|
||||
exports.intOct = intOct;
|
||||
25
bundled-addons/pithos/vendor/yaml/lib/schema/core/schema.js
vendored
Normal file
25
bundled-addons/pithos/vendor/yaml/lib/schema/core/schema.js
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
var map = require('../common/map.js');
|
||||
var _null = require('../common/null.js');
|
||||
var seq = require('../common/seq.js');
|
||||
var string = require('../common/string.js');
|
||||
var bool = require('./bool.js');
|
||||
var float = require('./float.js');
|
||||
var int = require('./int.js');
|
||||
|
||||
const schema = [
|
||||
map.map,
|
||||
seq.seq,
|
||||
string.string,
|
||||
_null.nullTag,
|
||||
bool.boolTag,
|
||||
int.intOct,
|
||||
int.int,
|
||||
int.intHex,
|
||||
float.floatNaN,
|
||||
float.floatExp,
|
||||
float.float
|
||||
];
|
||||
|
||||
exports.schema = schema;
|
||||
64
bundled-addons/pithos/vendor/yaml/lib/schema/json/schema.js
vendored
Normal file
64
bundled-addons/pithos/vendor/yaml/lib/schema/json/schema.js
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
var map = require('../common/map.js');
|
||||
var seq = require('../common/seq.js');
|
||||
|
||||
function intIdentify(value) {
|
||||
return typeof value === 'bigint' || Number.isInteger(value);
|
||||
}
|
||||
const stringifyJSON = ({ value }) => JSON.stringify(value);
|
||||
const jsonScalars = [
|
||||
{
|
||||
identify: value => typeof value === 'string',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:str',
|
||||
resolve: str => str,
|
||||
stringify: stringifyJSON
|
||||
},
|
||||
{
|
||||
identify: value => value == null,
|
||||
createNode: () => new Scalar.Scalar(null),
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:null',
|
||||
test: /^null$/,
|
||||
resolve: () => null,
|
||||
stringify: stringifyJSON
|
||||
},
|
||||
{
|
||||
identify: value => typeof value === 'boolean',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:bool',
|
||||
test: /^true$|^false$/,
|
||||
resolve: str => str === 'true',
|
||||
stringify: stringifyJSON
|
||||
},
|
||||
{
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
test: /^-?(?:0|[1-9][0-9]*)$/,
|
||||
resolve: (str, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str, 10),
|
||||
stringify: ({ value }) => intIdentify(value) ? value.toString() : JSON.stringify(value)
|
||||
},
|
||||
{
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/,
|
||||
resolve: str => parseFloat(str),
|
||||
stringify: stringifyJSON
|
||||
}
|
||||
];
|
||||
const jsonError = {
|
||||
default: true,
|
||||
tag: '',
|
||||
test: /^/,
|
||||
resolve(str, onError) {
|
||||
onError(`Unresolved plain scalar ${JSON.stringify(str)}`);
|
||||
return str;
|
||||
}
|
||||
};
|
||||
const schema = [map.map, seq.seq].concat(jsonScalars, jsonError);
|
||||
|
||||
exports.schema = schema;
|
||||
99
bundled-addons/pithos/vendor/yaml/lib/schema/tags.js
vendored
Normal file
99
bundled-addons/pithos/vendor/yaml/lib/schema/tags.js
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
var map = require('./common/map.js');
|
||||
var _null = require('./common/null.js');
|
||||
var seq = require('./common/seq.js');
|
||||
var string = require('./common/string.js');
|
||||
var bool = require('./core/bool.js');
|
||||
var float = require('./core/float.js');
|
||||
var int = require('./core/int.js');
|
||||
var schema = require('./core/schema.js');
|
||||
var schema$1 = require('./json/schema.js');
|
||||
var binary = require('./yaml-1.1/binary.js');
|
||||
var merge = require('./yaml-1.1/merge.js');
|
||||
var omap = require('./yaml-1.1/omap.js');
|
||||
var pairs = require('./yaml-1.1/pairs.js');
|
||||
var schema$2 = require('./yaml-1.1/schema.js');
|
||||
var set = require('./yaml-1.1/set.js');
|
||||
var timestamp = require('./yaml-1.1/timestamp.js');
|
||||
|
||||
const schemas = new Map([
|
||||
['core', schema.schema],
|
||||
['failsafe', [map.map, seq.seq, string.string]],
|
||||
['json', schema$1.schema],
|
||||
['yaml11', schema$2.schema],
|
||||
['yaml-1.1', schema$2.schema]
|
||||
]);
|
||||
const tagsByName = {
|
||||
binary: binary.binary,
|
||||
bool: bool.boolTag,
|
||||
float: float.float,
|
||||
floatExp: float.floatExp,
|
||||
floatNaN: float.floatNaN,
|
||||
floatTime: timestamp.floatTime,
|
||||
int: int.int,
|
||||
intHex: int.intHex,
|
||||
intOct: int.intOct,
|
||||
intTime: timestamp.intTime,
|
||||
map: map.map,
|
||||
merge: merge.merge,
|
||||
null: _null.nullTag,
|
||||
omap: omap.omap,
|
||||
pairs: pairs.pairs,
|
||||
seq: seq.seq,
|
||||
set: set.set,
|
||||
timestamp: timestamp.timestamp
|
||||
};
|
||||
const coreKnownTags = {
|
||||
'tag:yaml.org,2002:binary': binary.binary,
|
||||
'tag:yaml.org,2002:merge': merge.merge,
|
||||
'tag:yaml.org,2002:omap': omap.omap,
|
||||
'tag:yaml.org,2002:pairs': pairs.pairs,
|
||||
'tag:yaml.org,2002:set': set.set,
|
||||
'tag:yaml.org,2002:timestamp': timestamp.timestamp
|
||||
};
|
||||
function getTags(customTags, schemaName, addMergeTag) {
|
||||
const schemaTags = schemas.get(schemaName);
|
||||
if (schemaTags && !customTags) {
|
||||
return addMergeTag && !schemaTags.includes(merge.merge)
|
||||
? schemaTags.concat(merge.merge)
|
||||
: schemaTags.slice();
|
||||
}
|
||||
let tags = schemaTags;
|
||||
if (!tags) {
|
||||
if (Array.isArray(customTags))
|
||||
tags = [];
|
||||
else {
|
||||
const keys = Array.from(schemas.keys())
|
||||
.filter(key => key !== 'yaml11')
|
||||
.map(key => JSON.stringify(key))
|
||||
.join(', ');
|
||||
throw new Error(`Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(customTags)) {
|
||||
for (const tag of customTags)
|
||||
tags = tags.concat(tag);
|
||||
}
|
||||
else if (typeof customTags === 'function') {
|
||||
tags = customTags(tags.slice());
|
||||
}
|
||||
if (addMergeTag)
|
||||
tags = tags.concat(merge.merge);
|
||||
return tags.reduce((tags, tag) => {
|
||||
const tagObj = typeof tag === 'string' ? tagsByName[tag] : tag;
|
||||
if (!tagObj) {
|
||||
const tagName = JSON.stringify(tag);
|
||||
const keys = Object.keys(tagsByName)
|
||||
.map(key => JSON.stringify(key))
|
||||
.join(', ');
|
||||
throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`);
|
||||
}
|
||||
if (!tags.includes(tagObj))
|
||||
tags.push(tagObj);
|
||||
return tags;
|
||||
}, []);
|
||||
}
|
||||
|
||||
exports.coreKnownTags = coreKnownTags;
|
||||
exports.getTags = getTags;
|
||||
70
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/binary.js
vendored
Normal file
70
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/binary.js
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
var node_buffer = require('buffer');
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
var stringifyString = require('../../stringify/stringifyString.js');
|
||||
|
||||
const binary = {
|
||||
identify: value => value instanceof Uint8Array, // Buffer inherits from Uint8Array
|
||||
default: false,
|
||||
tag: 'tag:yaml.org,2002:binary',
|
||||
/**
|
||||
* Returns a Buffer in node and an Uint8Array in browsers
|
||||
*
|
||||
* To use the resulting buffer as an image, you'll want to do something like:
|
||||
*
|
||||
* const blob = new Blob([buffer], { type: 'image/jpeg' })
|
||||
* document.querySelector('#photo').src = URL.createObjectURL(blob)
|
||||
*/
|
||||
resolve(src, onError) {
|
||||
if (typeof node_buffer.Buffer === 'function') {
|
||||
return node_buffer.Buffer.from(src, 'base64');
|
||||
}
|
||||
else if (typeof atob === 'function') {
|
||||
// On IE 11, atob() can't handle newlines
|
||||
const str = atob(src.replace(/[\n\r]/g, ''));
|
||||
const buffer = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; ++i)
|
||||
buffer[i] = str.charCodeAt(i);
|
||||
return buffer;
|
||||
}
|
||||
else {
|
||||
onError('This environment does not support reading binary tags; either Buffer or atob is required');
|
||||
return src;
|
||||
}
|
||||
},
|
||||
stringify({ comment, type, value }, ctx, onComment, onChompKeep) {
|
||||
if (!value)
|
||||
return '';
|
||||
const buf = value; // checked earlier by binary.identify()
|
||||
let str;
|
||||
if (typeof node_buffer.Buffer === 'function') {
|
||||
str =
|
||||
buf instanceof node_buffer.Buffer
|
||||
? buf.toString('base64')
|
||||
: node_buffer.Buffer.from(buf.buffer).toString('base64');
|
||||
}
|
||||
else if (typeof btoa === 'function') {
|
||||
let s = '';
|
||||
for (let i = 0; i < buf.length; ++i)
|
||||
s += String.fromCharCode(buf[i]);
|
||||
str = btoa(s);
|
||||
}
|
||||
else {
|
||||
throw new Error('This environment does not support writing binary tags; either Buffer or btoa is required');
|
||||
}
|
||||
type ?? (type = Scalar.Scalar.BLOCK_LITERAL);
|
||||
if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
|
||||
const lineWidth = Math.max(ctx.options.lineWidth - ctx.indent.length, ctx.options.minContentWidth);
|
||||
const n = Math.ceil(str.length / lineWidth);
|
||||
const lines = new Array(n);
|
||||
for (let i = 0, o = 0; i < n; ++i, o += lineWidth) {
|
||||
lines[i] = str.substr(o, lineWidth);
|
||||
}
|
||||
str = lines.join(type === Scalar.Scalar.BLOCK_LITERAL ? '\n' : ' ');
|
||||
}
|
||||
return stringifyString.stringifyString({ comment, type, value: str }, ctx, onComment, onChompKeep);
|
||||
}
|
||||
};
|
||||
|
||||
exports.binary = binary;
|
||||
29
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/bool.js
vendored
Normal file
29
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/bool.js
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
|
||||
function boolStringify({ value, source }, ctx) {
|
||||
const boolObj = value ? trueTag : falseTag;
|
||||
if (source && boolObj.test.test(source))
|
||||
return source;
|
||||
return value ? ctx.options.trueStr : ctx.options.falseStr;
|
||||
}
|
||||
const trueTag = {
|
||||
identify: value => value === true,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:bool',
|
||||
test: /^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/,
|
||||
resolve: () => new Scalar.Scalar(true),
|
||||
stringify: boolStringify
|
||||
};
|
||||
const falseTag = {
|
||||
identify: value => value === false,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:bool',
|
||||
test: /^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/,
|
||||
resolve: () => new Scalar.Scalar(false),
|
||||
stringify: boolStringify
|
||||
};
|
||||
|
||||
exports.falseTag = falseTag;
|
||||
exports.trueTag = trueTag;
|
||||
50
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/float.js
vendored
Normal file
50
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/float.js
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
var stringifyNumber = require('../../stringify/stringifyNumber.js');
|
||||
|
||||
const floatNaN = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,
|
||||
resolve: (str) => str.slice(-3).toLowerCase() === 'nan'
|
||||
? NaN
|
||||
: str[0] === '-'
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: Number.POSITIVE_INFINITY,
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
const floatExp = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
format: 'EXP',
|
||||
test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/,
|
||||
resolve: (str) => parseFloat(str.replace(/_/g, '')),
|
||||
stringify(node) {
|
||||
const num = Number(node.value);
|
||||
return isFinite(num) ? num.toExponential() : stringifyNumber.stringifyNumber(node);
|
||||
}
|
||||
};
|
||||
const float = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/,
|
||||
resolve(str) {
|
||||
const node = new Scalar.Scalar(parseFloat(str.replace(/_/g, '')));
|
||||
const dot = str.indexOf('.');
|
||||
if (dot !== -1) {
|
||||
const f = str.substring(dot + 1).replace(/_/g, '');
|
||||
if (f[f.length - 1] === '0')
|
||||
node.minFractionDigits = f.length;
|
||||
}
|
||||
return node;
|
||||
},
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
|
||||
exports.float = float;
|
||||
exports.floatExp = floatExp;
|
||||
exports.floatNaN = floatNaN;
|
||||
76
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/int.js
vendored
Normal file
76
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/int.js
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
'use strict';
|
||||
|
||||
var stringifyNumber = require('../../stringify/stringifyNumber.js');
|
||||
|
||||
const intIdentify = (value) => typeof value === 'bigint' || Number.isInteger(value);
|
||||
function intResolve(str, offset, radix, { intAsBigInt }) {
|
||||
const sign = str[0];
|
||||
if (sign === '-' || sign === '+')
|
||||
offset += 1;
|
||||
str = str.substring(offset).replace(/_/g, '');
|
||||
if (intAsBigInt) {
|
||||
switch (radix) {
|
||||
case 2:
|
||||
str = `0b${str}`;
|
||||
break;
|
||||
case 8:
|
||||
str = `0o${str}`;
|
||||
break;
|
||||
case 16:
|
||||
str = `0x${str}`;
|
||||
break;
|
||||
}
|
||||
const n = BigInt(str);
|
||||
return sign === '-' ? BigInt(-1) * n : n;
|
||||
}
|
||||
const n = parseInt(str, radix);
|
||||
return sign === '-' ? -1 * n : n;
|
||||
}
|
||||
function intStringify(node, radix, prefix) {
|
||||
const { value } = node;
|
||||
if (intIdentify(value)) {
|
||||
const str = value.toString(radix);
|
||||
return value < 0 ? '-' + prefix + str.substr(1) : prefix + str;
|
||||
}
|
||||
return stringifyNumber.stringifyNumber(node);
|
||||
}
|
||||
const intBin = {
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'BIN',
|
||||
test: /^[-+]?0b[0-1_]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 2, 2, opt),
|
||||
stringify: node => intStringify(node, 2, '0b')
|
||||
};
|
||||
const intOct = {
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'OCT',
|
||||
test: /^[-+]?0[0-7_]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 1, 8, opt),
|
||||
stringify: node => intStringify(node, 8, '0')
|
||||
};
|
||||
const int = {
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
test: /^[-+]?[0-9][0-9_]*$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt),
|
||||
stringify: stringifyNumber.stringifyNumber
|
||||
};
|
||||
const intHex = {
|
||||
identify: intIdentify,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'HEX',
|
||||
test: /^[-+]?0x[0-9a-fA-F_]+$/,
|
||||
resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt),
|
||||
stringify: node => intStringify(node, 16, '0x')
|
||||
};
|
||||
|
||||
exports.int = int;
|
||||
exports.intBin = intBin;
|
||||
exports.intHex = intHex;
|
||||
exports.intOct = intOct;
|
||||
71
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/merge.js
vendored
Normal file
71
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/merge.js
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
|
||||
// If the value associated with a merge key is a single mapping node, each of
|
||||
// its key/value pairs is inserted into the current mapping, unless the key
|
||||
// already exists in it. If the value associated with the merge key is a
|
||||
// sequence, then this sequence is expected to contain mapping nodes and each
|
||||
// of these nodes is merged in turn according to its order in the sequence.
|
||||
// Keys in mapping nodes earlier in the sequence override keys specified in
|
||||
// later mapping nodes. -- http://yaml.org/type/merge.html
|
||||
const MERGE_KEY = '<<';
|
||||
const merge = {
|
||||
identify: value => value === MERGE_KEY ||
|
||||
(typeof value === 'symbol' && value.description === MERGE_KEY),
|
||||
default: 'key',
|
||||
tag: 'tag:yaml.org,2002:merge',
|
||||
test: /^<<$/,
|
||||
resolve: () => Object.assign(new Scalar.Scalar(Symbol(MERGE_KEY)), {
|
||||
addToJSMap: addMergeToJSMap
|
||||
}),
|
||||
stringify: () => MERGE_KEY
|
||||
};
|
||||
const isMergeKey = (ctx, key) => (merge.identify(key) ||
|
||||
(identity.isScalar(key) &&
|
||||
(!key.type || key.type === Scalar.Scalar.PLAIN) &&
|
||||
merge.identify(key.value))) &&
|
||||
ctx?.doc.schema.tags.some(tag => tag.tag === merge.tag && tag.default);
|
||||
function addMergeToJSMap(ctx, map, value) {
|
||||
const source = resolveAliasValue(ctx, value);
|
||||
if (identity.isSeq(source))
|
||||
for (const it of source.items)
|
||||
mergeValue(ctx, map, it);
|
||||
else if (Array.isArray(source))
|
||||
for (const it of source)
|
||||
mergeValue(ctx, map, it);
|
||||
else
|
||||
mergeValue(ctx, map, source);
|
||||
}
|
||||
function mergeValue(ctx, map, value) {
|
||||
const source = resolveAliasValue(ctx, value);
|
||||
if (!identity.isMap(source))
|
||||
throw new Error('Merge sources must be maps or map aliases');
|
||||
const srcMap = source.toJSON(null, ctx, Map);
|
||||
for (const [key, value] of srcMap) {
|
||||
if (map instanceof Map) {
|
||||
if (!map.has(key))
|
||||
map.set(key, value);
|
||||
}
|
||||
else if (map instanceof Set) {
|
||||
map.add(key);
|
||||
}
|
||||
else if (!Object.prototype.hasOwnProperty.call(map, key)) {
|
||||
Object.defineProperty(map, key, {
|
||||
value,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
function resolveAliasValue(ctx, value) {
|
||||
return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value;
|
||||
}
|
||||
|
||||
exports.addMergeToJSMap = addMergeToJSMap;
|
||||
exports.isMergeKey = isMergeKey;
|
||||
exports.merge = merge;
|
||||
77
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/omap.js
vendored
Normal file
77
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/omap.js
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var toJS = require('../../nodes/toJS.js');
|
||||
var YAMLMap = require('../../nodes/YAMLMap.js');
|
||||
var YAMLSeq = require('../../nodes/YAMLSeq.js');
|
||||
var pairs = require('./pairs.js');
|
||||
|
||||
class YAMLOMap extends YAMLSeq.YAMLSeq {
|
||||
constructor() {
|
||||
super();
|
||||
this.add = YAMLMap.YAMLMap.prototype.add.bind(this);
|
||||
this.delete = YAMLMap.YAMLMap.prototype.delete.bind(this);
|
||||
this.get = YAMLMap.YAMLMap.prototype.get.bind(this);
|
||||
this.has = YAMLMap.YAMLMap.prototype.has.bind(this);
|
||||
this.set = YAMLMap.YAMLMap.prototype.set.bind(this);
|
||||
this.tag = YAMLOMap.tag;
|
||||
}
|
||||
/**
|
||||
* If `ctx` is given, the return type is actually `Map<unknown, unknown>`,
|
||||
* but TypeScript won't allow widening the signature of a child method.
|
||||
*/
|
||||
toJSON(_, ctx) {
|
||||
if (!ctx)
|
||||
return super.toJSON(_);
|
||||
const map = new Map();
|
||||
if (ctx?.onCreate)
|
||||
ctx.onCreate(map);
|
||||
for (const pair of this.items) {
|
||||
let key, value;
|
||||
if (identity.isPair(pair)) {
|
||||
key = toJS.toJS(pair.key, '', ctx);
|
||||
value = toJS.toJS(pair.value, key, ctx);
|
||||
}
|
||||
else {
|
||||
key = toJS.toJS(pair, '', ctx);
|
||||
}
|
||||
if (map.has(key))
|
||||
throw new Error('Ordered maps must not include duplicate keys');
|
||||
map.set(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
static from(schema, iterable, ctx) {
|
||||
const pairs$1 = pairs.createPairs(schema, iterable, ctx);
|
||||
const omap = new this();
|
||||
omap.items = pairs$1.items;
|
||||
return omap;
|
||||
}
|
||||
}
|
||||
YAMLOMap.tag = 'tag:yaml.org,2002:omap';
|
||||
const omap = {
|
||||
collection: 'seq',
|
||||
identify: value => value instanceof Map,
|
||||
nodeClass: YAMLOMap,
|
||||
default: false,
|
||||
tag: 'tag:yaml.org,2002:omap',
|
||||
resolve(seq, onError) {
|
||||
const pairs$1 = pairs.resolvePairs(seq, onError);
|
||||
const seenKeys = [];
|
||||
for (const { key } of pairs$1.items) {
|
||||
if (identity.isScalar(key)) {
|
||||
if (seenKeys.includes(key.value)) {
|
||||
onError(`Ordered maps must not include duplicate keys: ${key.value}`);
|
||||
}
|
||||
else {
|
||||
seenKeys.push(key.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.assign(new YAMLOMap(), pairs$1);
|
||||
},
|
||||
createNode: (schema, iterable, ctx) => YAMLOMap.from(schema, iterable, ctx)
|
||||
};
|
||||
|
||||
exports.YAMLOMap = YAMLOMap;
|
||||
exports.omap = omap;
|
||||
82
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/pairs.js
vendored
Normal file
82
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/pairs.js
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var Pair = require('../../nodes/Pair.js');
|
||||
var Scalar = require('../../nodes/Scalar.js');
|
||||
var YAMLSeq = require('../../nodes/YAMLSeq.js');
|
||||
|
||||
function resolvePairs(seq, onError) {
|
||||
if (identity.isSeq(seq)) {
|
||||
for (let i = 0; i < seq.items.length; ++i) {
|
||||
let item = seq.items[i];
|
||||
if (identity.isPair(item))
|
||||
continue;
|
||||
else if (identity.isMap(item)) {
|
||||
if (item.items.length > 1)
|
||||
onError('Each pair must have its own sequence indicator');
|
||||
const pair = item.items[0] || new Pair.Pair(new Scalar.Scalar(null));
|
||||
if (item.commentBefore)
|
||||
pair.key.commentBefore = pair.key.commentBefore
|
||||
? `${item.commentBefore}\n${pair.key.commentBefore}`
|
||||
: item.commentBefore;
|
||||
if (item.comment) {
|
||||
const cn = pair.value ?? pair.key;
|
||||
cn.comment = cn.comment
|
||||
? `${item.comment}\n${cn.comment}`
|
||||
: item.comment;
|
||||
}
|
||||
item = pair;
|
||||
}
|
||||
seq.items[i] = identity.isPair(item) ? item : new Pair.Pair(item);
|
||||
}
|
||||
}
|
||||
else
|
||||
onError('Expected a sequence for this tag');
|
||||
return seq;
|
||||
}
|
||||
function createPairs(schema, iterable, ctx) {
|
||||
const { replacer } = ctx;
|
||||
const pairs = new YAMLSeq.YAMLSeq(schema);
|
||||
pairs.tag = 'tag:yaml.org,2002:pairs';
|
||||
let i = 0;
|
||||
if (iterable && Symbol.iterator in Object(iterable))
|
||||
for (let it of iterable) {
|
||||
if (typeof replacer === 'function')
|
||||
it = replacer.call(iterable, String(i++), it);
|
||||
let key, value;
|
||||
if (Array.isArray(it)) {
|
||||
if (it.length === 2) {
|
||||
key = it[0];
|
||||
value = it[1];
|
||||
}
|
||||
else
|
||||
throw new TypeError(`Expected [key, value] tuple: ${it}`);
|
||||
}
|
||||
else if (it && it instanceof Object) {
|
||||
const keys = Object.keys(it);
|
||||
if (keys.length === 1) {
|
||||
key = keys[0];
|
||||
value = it[key];
|
||||
}
|
||||
else {
|
||||
throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
key = it;
|
||||
}
|
||||
pairs.items.push(Pair.createPair(key, value, ctx));
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
const pairs = {
|
||||
collection: 'seq',
|
||||
default: false,
|
||||
tag: 'tag:yaml.org,2002:pairs',
|
||||
resolve: resolvePairs,
|
||||
createNode: createPairs
|
||||
};
|
||||
|
||||
exports.createPairs = createPairs;
|
||||
exports.pairs = pairs;
|
||||
exports.resolvePairs = resolvePairs;
|
||||
41
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/schema.js
vendored
Normal file
41
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/schema.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
var map = require('../common/map.js');
|
||||
var _null = require('../common/null.js');
|
||||
var seq = require('../common/seq.js');
|
||||
var string = require('../common/string.js');
|
||||
var binary = require('./binary.js');
|
||||
var bool = require('./bool.js');
|
||||
var float = require('./float.js');
|
||||
var int = require('./int.js');
|
||||
var merge = require('./merge.js');
|
||||
var omap = require('./omap.js');
|
||||
var pairs = require('./pairs.js');
|
||||
var set = require('./set.js');
|
||||
var timestamp = require('./timestamp.js');
|
||||
|
||||
const schema = [
|
||||
map.map,
|
||||
seq.seq,
|
||||
string.string,
|
||||
_null.nullTag,
|
||||
bool.trueTag,
|
||||
bool.falseTag,
|
||||
int.intBin,
|
||||
int.intOct,
|
||||
int.int,
|
||||
int.intHex,
|
||||
float.floatNaN,
|
||||
float.floatExp,
|
||||
float.float,
|
||||
binary.binary,
|
||||
merge.merge,
|
||||
omap.omap,
|
||||
pairs.pairs,
|
||||
set.set,
|
||||
timestamp.intTime,
|
||||
timestamp.floatTime,
|
||||
timestamp.timestamp
|
||||
];
|
||||
|
||||
exports.schema = schema;
|
||||
96
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/set.js
vendored
Normal file
96
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/set.js
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../../nodes/identity.js');
|
||||
var Pair = require('../../nodes/Pair.js');
|
||||
var YAMLMap = require('../../nodes/YAMLMap.js');
|
||||
|
||||
class YAMLSet extends YAMLMap.YAMLMap {
|
||||
constructor(schema) {
|
||||
super(schema);
|
||||
this.tag = YAMLSet.tag;
|
||||
}
|
||||
add(key) {
|
||||
let pair;
|
||||
if (identity.isPair(key))
|
||||
pair = key;
|
||||
else if (key &&
|
||||
typeof key === 'object' &&
|
||||
'key' in key &&
|
||||
'value' in key &&
|
||||
key.value === null)
|
||||
pair = new Pair.Pair(key.key, null);
|
||||
else
|
||||
pair = new Pair.Pair(key, null);
|
||||
const prev = YAMLMap.findPair(this.items, pair.key);
|
||||
if (!prev)
|
||||
this.items.push(pair);
|
||||
}
|
||||
/**
|
||||
* If `keepPair` is `true`, returns the Pair matching `key`.
|
||||
* Otherwise, returns the value of that Pair's key.
|
||||
*/
|
||||
get(key, keepPair) {
|
||||
const pair = YAMLMap.findPair(this.items, key);
|
||||
return !keepPair && identity.isPair(pair)
|
||||
? identity.isScalar(pair.key)
|
||||
? pair.key.value
|
||||
: pair.key
|
||||
: pair;
|
||||
}
|
||||
set(key, value) {
|
||||
if (typeof value !== 'boolean')
|
||||
throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`);
|
||||
const prev = YAMLMap.findPair(this.items, key);
|
||||
if (prev && !value) {
|
||||
this.items.splice(this.items.indexOf(prev), 1);
|
||||
}
|
||||
else if (!prev && value) {
|
||||
this.items.push(new Pair.Pair(key));
|
||||
}
|
||||
}
|
||||
toJSON(_, ctx) {
|
||||
return super.toJSON(_, ctx, Set);
|
||||
}
|
||||
toString(ctx, onComment, onChompKeep) {
|
||||
if (!ctx)
|
||||
return JSON.stringify(this);
|
||||
if (this.hasAllNullValues(true))
|
||||
return super.toString(Object.assign({}, ctx, { allNullValues: true }), onComment, onChompKeep);
|
||||
else
|
||||
throw new Error('Set items must all have null values');
|
||||
}
|
||||
static from(schema, iterable, ctx) {
|
||||
const { replacer } = ctx;
|
||||
const set = new this(schema);
|
||||
if (iterable && Symbol.iterator in Object(iterable))
|
||||
for (let value of iterable) {
|
||||
if (typeof replacer === 'function')
|
||||
value = replacer.call(iterable, value, value);
|
||||
set.items.push(Pair.createPair(value, null, ctx));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
YAMLSet.tag = 'tag:yaml.org,2002:set';
|
||||
const set = {
|
||||
collection: 'map',
|
||||
identify: value => value instanceof Set,
|
||||
nodeClass: YAMLSet,
|
||||
default: false,
|
||||
tag: 'tag:yaml.org,2002:set',
|
||||
createNode: (schema, iterable, ctx) => YAMLSet.from(schema, iterable, ctx),
|
||||
resolve(map, onError) {
|
||||
if (identity.isMap(map)) {
|
||||
if (map.hasAllNullValues(true))
|
||||
return Object.assign(new YAMLSet(), map);
|
||||
else
|
||||
onError('Set items must all have null values');
|
||||
}
|
||||
else
|
||||
onError('Expected a mapping for this tag');
|
||||
return map;
|
||||
}
|
||||
};
|
||||
|
||||
exports.YAMLSet = YAMLSet;
|
||||
exports.set = set;
|
||||
105
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/timestamp.js
vendored
Normal file
105
bundled-addons/pithos/vendor/yaml/lib/schema/yaml-1.1/timestamp.js
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
'use strict';
|
||||
|
||||
var stringifyNumber = require('../../stringify/stringifyNumber.js');
|
||||
|
||||
/** Internal types handle bigint as number, because TS can't figure it out. */
|
||||
function parseSexagesimal(str, asBigInt) {
|
||||
const sign = str[0];
|
||||
const parts = sign === '-' || sign === '+' ? str.substring(1) : str;
|
||||
const num = (n) => asBigInt ? BigInt(n) : Number(n);
|
||||
const res = parts
|
||||
.replace(/_/g, '')
|
||||
.split(':')
|
||||
.reduce((res, p) => res * num(60) + num(p), num(0));
|
||||
return (sign === '-' ? num(-1) * res : res);
|
||||
}
|
||||
/**
|
||||
* hhhh:mm:ss.sss
|
||||
*
|
||||
* Internal types handle bigint as number, because TS can't figure it out.
|
||||
*/
|
||||
function stringifySexagesimal(node) {
|
||||
let { value } = node;
|
||||
let num = (n) => n;
|
||||
if (typeof value === 'bigint')
|
||||
num = n => BigInt(n);
|
||||
else if (isNaN(value) || !isFinite(value))
|
||||
return stringifyNumber.stringifyNumber(node);
|
||||
let sign = '';
|
||||
if (value < 0) {
|
||||
sign = '-';
|
||||
value *= num(-1);
|
||||
}
|
||||
const _60 = num(60);
|
||||
const parts = [value % _60]; // seconds, including ms
|
||||
if (value < 60) {
|
||||
parts.unshift(0); // at least one : is required
|
||||
}
|
||||
else {
|
||||
value = (value - parts[0]) / _60;
|
||||
parts.unshift(value % _60); // minutes
|
||||
if (value >= 60) {
|
||||
value = (value - parts[0]) / _60;
|
||||
parts.unshift(value); // hours
|
||||
}
|
||||
}
|
||||
return (sign +
|
||||
parts
|
||||
.map(n => String(n).padStart(2, '0'))
|
||||
.join(':')
|
||||
.replace(/000000\d*$/, '') // % 60 may introduce error
|
||||
);
|
||||
}
|
||||
const intTime = {
|
||||
identify: value => typeof value === 'bigint' || Number.isInteger(value),
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:int',
|
||||
format: 'TIME',
|
||||
test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/,
|
||||
resolve: (str, _onError, { intAsBigInt }) => parseSexagesimal(str, intAsBigInt),
|
||||
stringify: stringifySexagesimal
|
||||
};
|
||||
const floatTime = {
|
||||
identify: value => typeof value === 'number',
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:float',
|
||||
format: 'TIME',
|
||||
test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/,
|
||||
resolve: str => parseSexagesimal(str, false),
|
||||
stringify: stringifySexagesimal
|
||||
};
|
||||
const timestamp = {
|
||||
identify: value => value instanceof Date,
|
||||
default: true,
|
||||
tag: 'tag:yaml.org,2002:timestamp',
|
||||
// If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part
|
||||
// may be omitted altogether, resulting in a date format. In such a case, the time part is
|
||||
// assumed to be 00:00:00Z (start of day, UTC).
|
||||
test: RegExp('^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})' + // YYYY-Mm-Dd
|
||||
'(?:' + // time is optional
|
||||
'(?:t|T|[ \\t]+)' + // t | T | whitespace
|
||||
'([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)' + // Hh:Mm:Ss(.ss)?
|
||||
'(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?' + // Z | +5 | -03:30
|
||||
')?$'),
|
||||
resolve(str) {
|
||||
const match = str.match(timestamp.test);
|
||||
if (!match)
|
||||
throw new Error('!!timestamp expects a date, starting with yyyy-mm-dd');
|
||||
const [, year, month, day, hour, minute, second] = match.map(Number);
|
||||
const millisec = match[7] ? Number((match[7] + '00').substr(1, 3)) : 0;
|
||||
let date = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec);
|
||||
const tz = match[8];
|
||||
if (tz && tz !== 'Z') {
|
||||
let d = parseSexagesimal(tz, false);
|
||||
if (Math.abs(d) < 30)
|
||||
d *= 60;
|
||||
date -= 60000 * d;
|
||||
}
|
||||
return new Date(date);
|
||||
},
|
||||
stringify: ({ value }) => value?.toISOString().replace(/(T00:00:00)?\.000Z$/, '') ?? ''
|
||||
};
|
||||
|
||||
exports.floatTime = floatTime;
|
||||
exports.intTime = intTime;
|
||||
exports.timestamp = timestamp;
|
||||
151
bundled-addons/pithos/vendor/yaml/lib/stringify/foldFlowLines.js
vendored
Normal file
151
bundled-addons/pithos/vendor/yaml/lib/stringify/foldFlowLines.js
vendored
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
'use strict';
|
||||
|
||||
const FOLD_FLOW = 'flow';
|
||||
const FOLD_BLOCK = 'block';
|
||||
const FOLD_QUOTED = 'quoted';
|
||||
/**
|
||||
* Tries to keep input at up to `lineWidth` characters, splitting only on spaces
|
||||
* not followed by newlines or spaces unless `mode` is `'quoted'`. Lines are
|
||||
* terminated with `\n` and started with `indent`.
|
||||
*/
|
||||
function foldFlowLines(text, indent, mode = 'flow', { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) {
|
||||
if (!lineWidth || lineWidth < 0)
|
||||
return text;
|
||||
if (lineWidth < minContentWidth)
|
||||
minContentWidth = 0;
|
||||
const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length);
|
||||
if (text.length <= endStep)
|
||||
return text;
|
||||
const folds = [];
|
||||
const escapedFolds = {};
|
||||
let end = lineWidth - indent.length;
|
||||
if (typeof indentAtStart === 'number') {
|
||||
if (indentAtStart > lineWidth - Math.max(2, minContentWidth))
|
||||
folds.push(0);
|
||||
else
|
||||
end = lineWidth - indentAtStart;
|
||||
}
|
||||
let split = undefined;
|
||||
let prev = undefined;
|
||||
let overflow = false;
|
||||
let i = -1;
|
||||
let escStart = -1;
|
||||
let escEnd = -1;
|
||||
if (mode === FOLD_BLOCK) {
|
||||
i = consumeMoreIndentedLines(text, i, indent.length);
|
||||
if (i !== -1)
|
||||
end = i + endStep;
|
||||
}
|
||||
for (let ch; (ch = text[(i += 1)]);) {
|
||||
if (mode === FOLD_QUOTED && ch === '\\') {
|
||||
escStart = i;
|
||||
switch (text[i + 1]) {
|
||||
case 'x':
|
||||
i += 3;
|
||||
break;
|
||||
case 'u':
|
||||
i += 5;
|
||||
break;
|
||||
case 'U':
|
||||
i += 9;
|
||||
break;
|
||||
default:
|
||||
i += 1;
|
||||
}
|
||||
escEnd = i;
|
||||
}
|
||||
if (ch === '\n') {
|
||||
if (mode === FOLD_BLOCK)
|
||||
i = consumeMoreIndentedLines(text, i, indent.length);
|
||||
end = i + indent.length + endStep;
|
||||
split = undefined;
|
||||
}
|
||||
else {
|
||||
if (ch === ' ' &&
|
||||
prev &&
|
||||
prev !== ' ' &&
|
||||
prev !== '\n' &&
|
||||
prev !== '\t') {
|
||||
// space surrounded by non-space can be replaced with newline + indent
|
||||
const next = text[i + 1];
|
||||
if (next && next !== ' ' && next !== '\n' && next !== '\t')
|
||||
split = i;
|
||||
}
|
||||
if (i >= end) {
|
||||
if (split) {
|
||||
folds.push(split);
|
||||
end = split + endStep;
|
||||
split = undefined;
|
||||
}
|
||||
else if (mode === FOLD_QUOTED) {
|
||||
// white-space collected at end may stretch past lineWidth
|
||||
while (prev === ' ' || prev === '\t') {
|
||||
prev = ch;
|
||||
ch = text[(i += 1)];
|
||||
overflow = true;
|
||||
}
|
||||
// Account for newline escape, but don't break preceding escape
|
||||
const j = i > escEnd + 1 ? i - 2 : escStart - 1;
|
||||
// Bail out if lineWidth & minContentWidth are shorter than an escape string
|
||||
if (escapedFolds[j])
|
||||
return text;
|
||||
folds.push(j);
|
||||
escapedFolds[j] = true;
|
||||
end = j + endStep;
|
||||
split = undefined;
|
||||
}
|
||||
else {
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = ch;
|
||||
}
|
||||
if (overflow && onOverflow)
|
||||
onOverflow();
|
||||
if (folds.length === 0)
|
||||
return text;
|
||||
if (onFold)
|
||||
onFold();
|
||||
let res = text.slice(0, folds[0]);
|
||||
for (let i = 0; i < folds.length; ++i) {
|
||||
const fold = folds[i];
|
||||
const end = folds[i + 1] || text.length;
|
||||
if (fold === 0)
|
||||
res = `\n${indent}${text.slice(0, end)}`;
|
||||
else {
|
||||
if (mode === FOLD_QUOTED && escapedFolds[fold])
|
||||
res += `${text[fold]}\\`;
|
||||
res += `\n${indent}${text.slice(fold + 1, end)}`;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* Presumes `i + 1` is at the start of a line
|
||||
* @returns index of last newline in more-indented block
|
||||
*/
|
||||
function consumeMoreIndentedLines(text, i, indent) {
|
||||
let end = i;
|
||||
let start = i + 1;
|
||||
let ch = text[start];
|
||||
while (ch === ' ' || ch === '\t') {
|
||||
if (i < start + indent) {
|
||||
ch = text[++i];
|
||||
}
|
||||
else {
|
||||
do {
|
||||
ch = text[++i];
|
||||
} while (ch && ch !== '\n');
|
||||
end = i;
|
||||
start = i + 1;
|
||||
ch = text[start];
|
||||
}
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
exports.FOLD_BLOCK = FOLD_BLOCK;
|
||||
exports.FOLD_FLOW = FOLD_FLOW;
|
||||
exports.FOLD_QUOTED = FOLD_QUOTED;
|
||||
exports.foldFlowLines = foldFlowLines;
|
||||
132
bundled-addons/pithos/vendor/yaml/lib/stringify/stringify.js
vendored
Normal file
132
bundled-addons/pithos/vendor/yaml/lib/stringify/stringify.js
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
'use strict';
|
||||
|
||||
var anchors = require('../doc/anchors.js');
|
||||
var identity = require('../nodes/identity.js');
|
||||
var stringifyComment = require('./stringifyComment.js');
|
||||
var stringifyString = require('./stringifyString.js');
|
||||
|
||||
function createStringifyContext(doc, options) {
|
||||
const opt = Object.assign({
|
||||
blockQuote: true,
|
||||
commentString: stringifyComment.stringifyComment,
|
||||
defaultKeyType: null,
|
||||
defaultStringType: 'PLAIN',
|
||||
directives: null,
|
||||
doubleQuotedAsJSON: false,
|
||||
doubleQuotedMinMultiLineLength: 40,
|
||||
falseStr: 'false',
|
||||
flowCollectionPadding: true,
|
||||
indentSeq: true,
|
||||
lineWidth: 80,
|
||||
minContentWidth: 20,
|
||||
nullStr: 'null',
|
||||
simpleKeys: false,
|
||||
singleQuote: null,
|
||||
trailingComma: false,
|
||||
trueStr: 'true',
|
||||
verifyAliasOrder: true
|
||||
}, doc.schema.toStringOptions, options);
|
||||
let inFlow;
|
||||
switch (opt.collectionStyle) {
|
||||
case 'block':
|
||||
inFlow = false;
|
||||
break;
|
||||
case 'flow':
|
||||
inFlow = true;
|
||||
break;
|
||||
default:
|
||||
inFlow = null;
|
||||
}
|
||||
return {
|
||||
anchors: new Set(),
|
||||
doc,
|
||||
flowCollectionPadding: opt.flowCollectionPadding ? ' ' : '',
|
||||
indent: '',
|
||||
indentStep: typeof opt.indent === 'number' ? ' '.repeat(opt.indent) : ' ',
|
||||
inFlow,
|
||||
options: opt
|
||||
};
|
||||
}
|
||||
function getTagObject(tags, item) {
|
||||
if (item.tag) {
|
||||
const match = tags.filter(t => t.tag === item.tag);
|
||||
if (match.length > 0)
|
||||
return match.find(t => t.format === item.format) ?? match[0];
|
||||
}
|
||||
let tagObj = undefined;
|
||||
let obj;
|
||||
if (identity.isScalar(item)) {
|
||||
obj = item.value;
|
||||
let match = tags.filter(t => t.identify?.(obj));
|
||||
if (match.length > 1) {
|
||||
const testMatch = match.filter(t => t.test);
|
||||
if (testMatch.length > 0)
|
||||
match = testMatch;
|
||||
}
|
||||
tagObj =
|
||||
match.find(t => t.format === item.format) ?? match.find(t => !t.format);
|
||||
}
|
||||
else {
|
||||
obj = item;
|
||||
tagObj = tags.find(t => t.nodeClass && obj instanceof t.nodeClass);
|
||||
}
|
||||
if (!tagObj) {
|
||||
const name = obj?.constructor?.name ?? (obj === null ? 'null' : typeof obj);
|
||||
throw new Error(`Tag not resolved for ${name} value`);
|
||||
}
|
||||
return tagObj;
|
||||
}
|
||||
// needs to be called before value stringifier to allow for circular anchor refs
|
||||
function stringifyProps(node, tagObj, { anchors: anchors$1, doc }) {
|
||||
if (!doc.directives)
|
||||
return '';
|
||||
const props = [];
|
||||
const anchor = (identity.isScalar(node) || identity.isCollection(node)) && node.anchor;
|
||||
if (anchor && anchors.anchorIsValid(anchor)) {
|
||||
anchors$1.add(anchor);
|
||||
props.push(`&${anchor}`);
|
||||
}
|
||||
const tag = node.tag ?? (tagObj.default ? null : tagObj.tag);
|
||||
if (tag)
|
||||
props.push(doc.directives.tagString(tag));
|
||||
return props.join(' ');
|
||||
}
|
||||
function stringify(item, ctx, onComment, onChompKeep) {
|
||||
if (identity.isPair(item))
|
||||
return item.toString(ctx, onComment, onChompKeep);
|
||||
if (identity.isAlias(item)) {
|
||||
if (ctx.doc.directives)
|
||||
return item.toString(ctx);
|
||||
if (ctx.resolvedAliases?.has(item)) {
|
||||
throw new TypeError(`Cannot stringify circular structure without alias nodes`);
|
||||
}
|
||||
else {
|
||||
if (ctx.resolvedAliases)
|
||||
ctx.resolvedAliases.add(item);
|
||||
else
|
||||
ctx.resolvedAliases = new Set([item]);
|
||||
item = item.resolve(ctx.doc);
|
||||
}
|
||||
}
|
||||
let tagObj = undefined;
|
||||
const node = identity.isNode(item)
|
||||
? item
|
||||
: ctx.doc.createNode(item, { onTagObj: o => (tagObj = o) });
|
||||
tagObj ?? (tagObj = getTagObject(ctx.doc.schema.tags, node));
|
||||
const props = stringifyProps(node, tagObj, ctx);
|
||||
if (props.length > 0)
|
||||
ctx.indentAtStart = (ctx.indentAtStart ?? 0) + props.length + 1;
|
||||
const str = typeof tagObj.stringify === 'function'
|
||||
? tagObj.stringify(node, ctx, onComment, onChompKeep)
|
||||
: identity.isScalar(node)
|
||||
? stringifyString.stringifyString(node, ctx, onComment, onChompKeep)
|
||||
: node.toString(ctx, onComment, onChompKeep);
|
||||
if (!props)
|
||||
return str;
|
||||
return identity.isScalar(node) || str[0] === '{' || str[0] === '['
|
||||
? `${props} ${str}`
|
||||
: `${props}\n${ctx.indent}${str}`;
|
||||
}
|
||||
|
||||
exports.createStringifyContext = createStringifyContext;
|
||||
exports.stringify = stringify;
|
||||
155
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyCollection.js
vendored
Normal file
155
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyCollection.js
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var stringify = require('./stringify.js');
|
||||
var stringifyComment = require('./stringifyComment.js');
|
||||
|
||||
function stringifyCollection(collection, ctx, options) {
|
||||
const flow = ctx.inFlow ?? collection.flow;
|
||||
const stringify = flow ? stringifyFlowCollection : stringifyBlockCollection;
|
||||
return stringify(collection, ctx, options);
|
||||
}
|
||||
function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) {
|
||||
const { indent, options: { commentString } } = ctx;
|
||||
const itemCtx = Object.assign({}, ctx, { indent: itemIndent, type: null });
|
||||
let chompKeep = false; // flag for the preceding node's status
|
||||
const lines = [];
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
let comment = null;
|
||||
if (identity.isNode(item)) {
|
||||
if (!chompKeep && item.spaceBefore)
|
||||
lines.push('');
|
||||
addCommentBefore(ctx, lines, item.commentBefore, chompKeep);
|
||||
if (item.comment)
|
||||
comment = item.comment;
|
||||
}
|
||||
else if (identity.isPair(item)) {
|
||||
const ik = identity.isNode(item.key) ? item.key : null;
|
||||
if (ik) {
|
||||
if (!chompKeep && ik.spaceBefore)
|
||||
lines.push('');
|
||||
addCommentBefore(ctx, lines, ik.commentBefore, chompKeep);
|
||||
}
|
||||
}
|
||||
chompKeep = false;
|
||||
let str = stringify.stringify(item, itemCtx, () => (comment = null), () => (chompKeep = true));
|
||||
if (comment)
|
||||
str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
|
||||
if (chompKeep && comment)
|
||||
chompKeep = false;
|
||||
lines.push(blockItemPrefix + str);
|
||||
}
|
||||
let str;
|
||||
if (lines.length === 0) {
|
||||
str = flowChars.start + flowChars.end;
|
||||
}
|
||||
else {
|
||||
str = lines[0];
|
||||
for (let i = 1; i < lines.length; ++i) {
|
||||
const line = lines[i];
|
||||
str += line ? `\n${indent}${line}` : '\n';
|
||||
}
|
||||
}
|
||||
if (comment) {
|
||||
str += '\n' + stringifyComment.indentComment(commentString(comment), indent);
|
||||
if (onComment)
|
||||
onComment();
|
||||
}
|
||||
else if (chompKeep && onChompKeep)
|
||||
onChompKeep();
|
||||
return str;
|
||||
}
|
||||
function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
|
||||
const { indent, indentStep, flowCollectionPadding: fcPadding, options: { commentString } } = ctx;
|
||||
itemIndent += indentStep;
|
||||
const itemCtx = Object.assign({}, ctx, {
|
||||
indent: itemIndent,
|
||||
inFlow: true,
|
||||
type: null
|
||||
});
|
||||
let reqNewline = false;
|
||||
let linesAtValue = 0;
|
||||
const lines = [];
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
let comment = null;
|
||||
if (identity.isNode(item)) {
|
||||
if (item.spaceBefore)
|
||||
lines.push('');
|
||||
addCommentBefore(ctx, lines, item.commentBefore, false);
|
||||
if (item.comment)
|
||||
comment = item.comment;
|
||||
}
|
||||
else if (identity.isPair(item)) {
|
||||
const ik = identity.isNode(item.key) ? item.key : null;
|
||||
if (ik) {
|
||||
if (ik.spaceBefore)
|
||||
lines.push('');
|
||||
addCommentBefore(ctx, lines, ik.commentBefore, false);
|
||||
if (ik.comment)
|
||||
reqNewline = true;
|
||||
}
|
||||
const iv = identity.isNode(item.value) ? item.value : null;
|
||||
if (iv) {
|
||||
if (iv.comment)
|
||||
comment = iv.comment;
|
||||
if (iv.commentBefore)
|
||||
reqNewline = true;
|
||||
}
|
||||
else if (item.value == null && ik?.comment) {
|
||||
comment = ik.comment;
|
||||
}
|
||||
}
|
||||
if (comment)
|
||||
reqNewline = true;
|
||||
let str = stringify.stringify(item, itemCtx, () => (comment = null));
|
||||
reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
|
||||
if (i < items.length - 1) {
|
||||
str += ',';
|
||||
}
|
||||
else if (ctx.options.trailingComma) {
|
||||
if (ctx.options.lineWidth > 0) {
|
||||
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
|
||||
(str.length + 2) >
|
||||
ctx.options.lineWidth);
|
||||
}
|
||||
if (reqNewline) {
|
||||
str += ',';
|
||||
}
|
||||
}
|
||||
if (comment)
|
||||
str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
|
||||
lines.push(str);
|
||||
linesAtValue = lines.length;
|
||||
}
|
||||
const { start, end } = flowChars;
|
||||
if (lines.length === 0) {
|
||||
return start + end;
|
||||
}
|
||||
else {
|
||||
if (!reqNewline) {
|
||||
const len = lines.reduce((sum, line) => sum + line.length + 2, 2);
|
||||
reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth;
|
||||
}
|
||||
if (reqNewline) {
|
||||
let str = start;
|
||||
for (const line of lines)
|
||||
str += line ? `\n${indentStep}${indent}${line}` : '\n';
|
||||
return `${str}\n${indent}${end}`;
|
||||
}
|
||||
else {
|
||||
return `${start}${fcPadding}${lines.join(' ')}${fcPadding}${end}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function addCommentBefore({ indent, options: { commentString } }, lines, comment, chompKeep) {
|
||||
if (comment && chompKeep)
|
||||
comment = comment.replace(/^\n+/, '');
|
||||
if (comment) {
|
||||
const ic = stringifyComment.indentComment(commentString(comment), indent);
|
||||
lines.push(ic.trimStart()); // Avoid double indent on first line
|
||||
}
|
||||
}
|
||||
|
||||
exports.stringifyCollection = stringifyCollection;
|
||||
24
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyComment.js
vendored
Normal file
24
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyComment.js
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Stringifies a comment.
|
||||
*
|
||||
* Empty comment lines are left empty,
|
||||
* lines consisting of a single space are replaced by `#`,
|
||||
* and all other lines are prefixed with a `#`.
|
||||
*/
|
||||
const stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, '#');
|
||||
function indentComment(comment, indent) {
|
||||
if (/^\n+$/.test(comment))
|
||||
return comment.substring(1);
|
||||
return indent ? comment.replace(/^(?! *$)/gm, indent) : comment;
|
||||
}
|
||||
const lineComment = (str, indent, comment) => str.endsWith('\n')
|
||||
? indentComment(comment, indent)
|
||||
: comment.includes('\n')
|
||||
? '\n' + indentComment(comment, indent)
|
||||
: (str.endsWith(' ') ? '' : ' ') + comment;
|
||||
|
||||
exports.indentComment = indentComment;
|
||||
exports.lineComment = lineComment;
|
||||
exports.stringifyComment = stringifyComment;
|
||||
87
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyDocument.js
vendored
Normal file
87
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyDocument.js
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var stringify = require('./stringify.js');
|
||||
var stringifyComment = require('./stringifyComment.js');
|
||||
|
||||
function stringifyDocument(doc, options) {
|
||||
const lines = [];
|
||||
let hasDirectives = options.directives === true;
|
||||
if (options.directives !== false && doc.directives) {
|
||||
const dir = doc.directives.toString(doc);
|
||||
if (dir) {
|
||||
lines.push(dir);
|
||||
hasDirectives = true;
|
||||
}
|
||||
else if (doc.directives.docStart)
|
||||
hasDirectives = true;
|
||||
}
|
||||
if (hasDirectives)
|
||||
lines.push('---');
|
||||
const ctx = stringify.createStringifyContext(doc, options);
|
||||
const { commentString } = ctx.options;
|
||||
if (doc.commentBefore) {
|
||||
if (lines.length !== 1)
|
||||
lines.unshift('');
|
||||
const cs = commentString(doc.commentBefore);
|
||||
lines.unshift(stringifyComment.indentComment(cs, ''));
|
||||
}
|
||||
let chompKeep = false;
|
||||
let contentComment = null;
|
||||
if (doc.contents) {
|
||||
if (identity.isNode(doc.contents)) {
|
||||
if (doc.contents.spaceBefore && hasDirectives)
|
||||
lines.push('');
|
||||
if (doc.contents.commentBefore) {
|
||||
const cs = commentString(doc.contents.commentBefore);
|
||||
lines.push(stringifyComment.indentComment(cs, ''));
|
||||
}
|
||||
// top-level block scalars need to be indented if followed by a comment
|
||||
ctx.forceBlockIndent = !!doc.comment;
|
||||
contentComment = doc.contents.comment;
|
||||
}
|
||||
const onChompKeep = contentComment ? undefined : () => (chompKeep = true);
|
||||
let body = stringify.stringify(doc.contents, ctx, () => (contentComment = null), onChompKeep);
|
||||
if (contentComment)
|
||||
body += stringifyComment.lineComment(body, '', commentString(contentComment));
|
||||
if ((body[0] === '|' || body[0] === '>') &&
|
||||
lines[lines.length - 1] === '---') {
|
||||
// Top-level block scalars with a preceding doc marker ought to use the
|
||||
// same line for their header.
|
||||
lines[lines.length - 1] = `--- ${body}`;
|
||||
}
|
||||
else
|
||||
lines.push(body);
|
||||
}
|
||||
else {
|
||||
lines.push(stringify.stringify(doc.contents, ctx));
|
||||
}
|
||||
if (doc.directives?.docEnd) {
|
||||
if (doc.comment) {
|
||||
const cs = commentString(doc.comment);
|
||||
if (cs.includes('\n')) {
|
||||
lines.push('...');
|
||||
lines.push(stringifyComment.indentComment(cs, ''));
|
||||
}
|
||||
else {
|
||||
lines.push(`... ${cs}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push('...');
|
||||
}
|
||||
}
|
||||
else {
|
||||
let dc = doc.comment;
|
||||
if (dc && chompKeep)
|
||||
dc = dc.replace(/^\n+/, '');
|
||||
if (dc) {
|
||||
if ((!chompKeep || contentComment) && lines[lines.length - 1] !== '')
|
||||
lines.push('');
|
||||
lines.push(stringifyComment.indentComment(commentString(dc), ''));
|
||||
}
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
exports.stringifyDocument = stringifyDocument;
|
||||
27
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyNumber.js
vendored
Normal file
27
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyNumber.js
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
function stringifyNumber({ format, minFractionDigits, tag, value }) {
|
||||
if (typeof value === 'bigint')
|
||||
return String(value);
|
||||
const num = typeof value === 'number' ? value : Number(value);
|
||||
if (!isFinite(num))
|
||||
return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf';
|
||||
let n = Object.is(value, -0) ? '-0' : JSON.stringify(value);
|
||||
if (!format &&
|
||||
minFractionDigits &&
|
||||
(!tag || tag === 'tag:yaml.org,2002:float') &&
|
||||
/^-?\d/.test(n) &&
|
||||
!n.includes('e')) {
|
||||
let i = n.indexOf('.');
|
||||
if (i < 0) {
|
||||
i = n.length;
|
||||
n += '.';
|
||||
}
|
||||
let d = minFractionDigits - (n.length - i - 1);
|
||||
while (d-- > 0)
|
||||
n += '0';
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
exports.stringifyNumber = stringifyNumber;
|
||||
152
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyPair.js
vendored
Normal file
152
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyPair.js
vendored
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('../nodes/identity.js');
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
var stringify = require('./stringify.js');
|
||||
var stringifyComment = require('./stringifyComment.js');
|
||||
|
||||
function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
|
||||
const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx;
|
||||
let keyComment = (identity.isNode(key) && key.comment) || null;
|
||||
if (simpleKeys) {
|
||||
if (keyComment) {
|
||||
throw new Error('With simple keys, key nodes cannot have comments');
|
||||
}
|
||||
if (identity.isCollection(key) || (!identity.isNode(key) && typeof key === 'object')) {
|
||||
const msg = 'With simple keys, collection cannot be used as a key value';
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
let explicitKey = !simpleKeys &&
|
||||
(!key ||
|
||||
(keyComment && value == null && !ctx.inFlow) ||
|
||||
identity.isCollection(key) ||
|
||||
(identity.isScalar(key)
|
||||
? key.type === Scalar.Scalar.BLOCK_FOLDED || key.type === Scalar.Scalar.BLOCK_LITERAL
|
||||
: typeof key === 'object'));
|
||||
ctx = Object.assign({}, ctx, {
|
||||
allNullValues: false,
|
||||
implicitKey: !explicitKey && (simpleKeys || !allNullValues),
|
||||
indent: indent + indentStep
|
||||
});
|
||||
let keyCommentDone = false;
|
||||
let chompKeep = false;
|
||||
let str = stringify.stringify(key, ctx, () => (keyCommentDone = true), () => (chompKeep = true));
|
||||
if (!explicitKey && !ctx.inFlow && str.length > 1024) {
|
||||
if (simpleKeys)
|
||||
throw new Error('With simple keys, single line scalar must not span more than 1024 characters');
|
||||
explicitKey = true;
|
||||
}
|
||||
if (ctx.inFlow) {
|
||||
if (allNullValues || value == null) {
|
||||
if (keyCommentDone && onComment)
|
||||
onComment();
|
||||
return str === '' ? '?' : explicitKey ? `? ${str}` : str;
|
||||
}
|
||||
}
|
||||
else if ((allNullValues && !simpleKeys) || (value == null && explicitKey)) {
|
||||
str = `? ${str}`;
|
||||
if (keyComment && !keyCommentDone) {
|
||||
str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment));
|
||||
}
|
||||
else if (chompKeep && onChompKeep)
|
||||
onChompKeep();
|
||||
return str;
|
||||
}
|
||||
if (keyCommentDone)
|
||||
keyComment = null;
|
||||
if (explicitKey) {
|
||||
if (keyComment)
|
||||
str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment));
|
||||
str = `? ${str}\n${indent}:`;
|
||||
}
|
||||
else {
|
||||
str = `${str}:`;
|
||||
if (keyComment)
|
||||
str += stringifyComment.lineComment(str, ctx.indent, commentString(keyComment));
|
||||
}
|
||||
let vsb, vcb, valueComment;
|
||||
if (identity.isNode(value)) {
|
||||
vsb = !!value.spaceBefore;
|
||||
vcb = value.commentBefore;
|
||||
valueComment = value.comment;
|
||||
}
|
||||
else {
|
||||
vsb = false;
|
||||
vcb = null;
|
||||
valueComment = null;
|
||||
if (value && typeof value === 'object')
|
||||
value = doc.createNode(value);
|
||||
}
|
||||
ctx.implicitKey = false;
|
||||
if (!explicitKey && !keyComment && identity.isScalar(value))
|
||||
ctx.indentAtStart = str.length + 1;
|
||||
chompKeep = false;
|
||||
if (!indentSeq &&
|
||||
indentStep.length >= 2 &&
|
||||
!ctx.inFlow &&
|
||||
!explicitKey &&
|
||||
identity.isSeq(value) &&
|
||||
!value.flow &&
|
||||
!value.tag &&
|
||||
!value.anchor) {
|
||||
// If indentSeq === false, consider '- ' as part of indentation where possible
|
||||
ctx.indent = ctx.indent.substring(2);
|
||||
}
|
||||
let valueCommentDone = false;
|
||||
const valueStr = stringify.stringify(value, ctx, () => (valueCommentDone = true), () => (chompKeep = true));
|
||||
let ws = ' ';
|
||||
if (keyComment || vsb || vcb) {
|
||||
ws = vsb ? '\n' : '';
|
||||
if (vcb) {
|
||||
const cs = commentString(vcb);
|
||||
ws += `\n${stringifyComment.indentComment(cs, ctx.indent)}`;
|
||||
}
|
||||
if (valueStr === '' && !ctx.inFlow) {
|
||||
if (ws === '\n' && valueComment)
|
||||
ws = '\n\n';
|
||||
}
|
||||
else {
|
||||
ws += `\n${ctx.indent}`;
|
||||
}
|
||||
}
|
||||
else if (!explicitKey && identity.isCollection(value)) {
|
||||
const vs0 = valueStr[0];
|
||||
const nl0 = valueStr.indexOf('\n');
|
||||
const hasNewline = nl0 !== -1;
|
||||
const flow = ctx.inFlow ?? value.flow ?? value.items.length === 0;
|
||||
if (hasNewline || !flow) {
|
||||
let hasPropsLine = false;
|
||||
if (hasNewline && (vs0 === '&' || vs0 === '!')) {
|
||||
let sp0 = valueStr.indexOf(' ');
|
||||
if (vs0 === '&' &&
|
||||
sp0 !== -1 &&
|
||||
sp0 < nl0 &&
|
||||
valueStr[sp0 + 1] === '!') {
|
||||
sp0 = valueStr.indexOf(' ', sp0 + 1);
|
||||
}
|
||||
if (sp0 === -1 || nl0 < sp0)
|
||||
hasPropsLine = true;
|
||||
}
|
||||
if (!hasPropsLine)
|
||||
ws = `\n${ctx.indent}`;
|
||||
}
|
||||
}
|
||||
else if (valueStr === '' || valueStr[0] === '\n') {
|
||||
ws = '';
|
||||
}
|
||||
str += ws + valueStr;
|
||||
if (ctx.inFlow) {
|
||||
if (valueCommentDone && onComment)
|
||||
onComment();
|
||||
}
|
||||
else if (valueComment && !valueCommentDone) {
|
||||
str += stringifyComment.lineComment(str, ctx.indent, commentString(valueComment));
|
||||
}
|
||||
else if (chompKeep && onChompKeep) {
|
||||
onChompKeep();
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
exports.stringifyPair = stringifyPair;
|
||||
338
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyString.js
vendored
Normal file
338
bundled-addons/pithos/vendor/yaml/lib/stringify/stringifyString.js
vendored
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
'use strict';
|
||||
|
||||
var Scalar = require('../nodes/Scalar.js');
|
||||
var foldFlowLines = require('./foldFlowLines.js');
|
||||
|
||||
const getFoldOptions = (ctx, isBlock) => ({
|
||||
indentAtStart: isBlock ? ctx.indent.length : ctx.indentAtStart,
|
||||
lineWidth: ctx.options.lineWidth,
|
||||
minContentWidth: ctx.options.minContentWidth
|
||||
});
|
||||
// Also checks for lines starting with %, as parsing the output as YAML 1.1 will
|
||||
// presume that's starting a new document.
|
||||
const containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str);
|
||||
function lineLengthOverLimit(str, lineWidth, indentLength) {
|
||||
if (!lineWidth || lineWidth < 0)
|
||||
return false;
|
||||
const limit = lineWidth - indentLength;
|
||||
const strLen = str.length;
|
||||
if (strLen <= limit)
|
||||
return false;
|
||||
for (let i = 0, start = 0; i < strLen; ++i) {
|
||||
if (str[i] === '\n') {
|
||||
if (i - start > limit)
|
||||
return true;
|
||||
start = i + 1;
|
||||
if (strLen - start <= limit)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function doubleQuotedString(value, ctx) {
|
||||
const json = JSON.stringify(value);
|
||||
if (ctx.options.doubleQuotedAsJSON)
|
||||
return json;
|
||||
const { implicitKey } = ctx;
|
||||
const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
|
||||
const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
|
||||
let str = '';
|
||||
let start = 0;
|
||||
for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
|
||||
if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') {
|
||||
// space before newline needs to be escaped to not be folded
|
||||
str += json.slice(start, i) + '\\ ';
|
||||
i += 1;
|
||||
start = i;
|
||||
ch = '\\';
|
||||
}
|
||||
if (ch === '\\')
|
||||
switch (json[i + 1]) {
|
||||
case 'u':
|
||||
{
|
||||
str += json.slice(start, i);
|
||||
const code = json.substr(i + 2, 4);
|
||||
switch (code) {
|
||||
case '0000':
|
||||
str += '\\0';
|
||||
break;
|
||||
case '0007':
|
||||
str += '\\a';
|
||||
break;
|
||||
case '000b':
|
||||
str += '\\v';
|
||||
break;
|
||||
case '001b':
|
||||
str += '\\e';
|
||||
break;
|
||||
case '0085':
|
||||
str += '\\N';
|
||||
break;
|
||||
case '00a0':
|
||||
str += '\\_';
|
||||
break;
|
||||
case '2028':
|
||||
str += '\\L';
|
||||
break;
|
||||
case '2029':
|
||||
str += '\\P';
|
||||
break;
|
||||
default:
|
||||
if (code.substr(0, 2) === '00')
|
||||
str += '\\x' + code.substr(2);
|
||||
else
|
||||
str += json.substr(i, 6);
|
||||
}
|
||||
i += 5;
|
||||
start = i + 1;
|
||||
}
|
||||
break;
|
||||
case 'n':
|
||||
if (implicitKey ||
|
||||
json[i + 2] === '"' ||
|
||||
json.length < minMultiLineLength) {
|
||||
i += 1;
|
||||
}
|
||||
else {
|
||||
// folding will eat first newline
|
||||
str += json.slice(start, i) + '\n\n';
|
||||
while (json[i + 2] === '\\' &&
|
||||
json[i + 3] === 'n' &&
|
||||
json[i + 4] !== '"') {
|
||||
str += '\n';
|
||||
i += 2;
|
||||
}
|
||||
str += indent;
|
||||
// space after newline needs to be escaped to not be folded
|
||||
if (json[i + 2] === ' ')
|
||||
str += '\\';
|
||||
i += 1;
|
||||
start = i + 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
str = start ? str + json.slice(start) : json;
|
||||
return implicitKey
|
||||
? str
|
||||
: foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_QUOTED, getFoldOptions(ctx, false));
|
||||
}
|
||||
function singleQuotedString(value, ctx) {
|
||||
if (ctx.options.singleQuote === false ||
|
||||
(ctx.implicitKey && value.includes('\n')) ||
|
||||
/[ \t]\n|\n[ \t]/.test(value) // single quoted string can't have leading or trailing whitespace around newline
|
||||
)
|
||||
return doubleQuotedString(value, ctx);
|
||||
const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : '');
|
||||
const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'";
|
||||
return ctx.implicitKey
|
||||
? res
|
||||
: foldFlowLines.foldFlowLines(res, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
|
||||
}
|
||||
function quotedString(value, ctx) {
|
||||
const { singleQuote } = ctx.options;
|
||||
let qs;
|
||||
if (singleQuote === false)
|
||||
qs = doubleQuotedString;
|
||||
else {
|
||||
const hasDouble = value.includes('"');
|
||||
const hasSingle = value.includes("'");
|
||||
if (hasDouble && !hasSingle)
|
||||
qs = singleQuotedString;
|
||||
else if (hasSingle && !hasDouble)
|
||||
qs = doubleQuotedString;
|
||||
else
|
||||
qs = singleQuote ? singleQuotedString : doubleQuotedString;
|
||||
}
|
||||
return qs(value, ctx);
|
||||
}
|
||||
// The negative lookbehind avoids a polynomial search,
|
||||
// but isn't supported yet on Safari: https://caniuse.com/js-regexp-lookbehind
|
||||
let blockEndNewlines;
|
||||
try {
|
||||
blockEndNewlines = new RegExp('(^|(?<!\n))\n+(?!\n|$)', 'g');
|
||||
}
|
||||
catch {
|
||||
blockEndNewlines = /\n+(?!\n|$)/g;
|
||||
}
|
||||
function blockString({ comment, type, value }, ctx, onComment, onChompKeep) {
|
||||
const { blockQuote, commentString, lineWidth } = ctx.options;
|
||||
// 1. Block can't end in whitespace unless the last line is non-empty.
|
||||
// 2. Strings consisting of only whitespace are best rendered explicitly.
|
||||
if (!blockQuote || /\n[\t ]+$/.test(value)) {
|
||||
return quotedString(value, ctx);
|
||||
}
|
||||
const indent = ctx.indent ||
|
||||
(ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : '');
|
||||
const literal = blockQuote === 'literal'
|
||||
? true
|
||||
: blockQuote === 'folded' || type === Scalar.Scalar.BLOCK_FOLDED
|
||||
? false
|
||||
: type === Scalar.Scalar.BLOCK_LITERAL
|
||||
? true
|
||||
: !lineLengthOverLimit(value, lineWidth, indent.length);
|
||||
if (!value)
|
||||
return literal ? '|\n' : '>\n';
|
||||
// determine chomping from whitespace at value end
|
||||
let chomp;
|
||||
let endStart;
|
||||
for (endStart = value.length; endStart > 0; --endStart) {
|
||||
const ch = value[endStart - 1];
|
||||
if (ch !== '\n' && ch !== '\t' && ch !== ' ')
|
||||
break;
|
||||
}
|
||||
let end = value.substring(endStart);
|
||||
const endNlPos = end.indexOf('\n');
|
||||
if (endNlPos === -1) {
|
||||
chomp = '-'; // strip
|
||||
}
|
||||
else if (value === end || endNlPos !== end.length - 1) {
|
||||
chomp = '+'; // keep
|
||||
if (onChompKeep)
|
||||
onChompKeep();
|
||||
}
|
||||
else {
|
||||
chomp = ''; // clip
|
||||
}
|
||||
if (end) {
|
||||
value = value.slice(0, -end.length);
|
||||
if (end[end.length - 1] === '\n')
|
||||
end = end.slice(0, -1);
|
||||
end = end.replace(blockEndNewlines, `$&${indent}`);
|
||||
}
|
||||
// determine indent indicator from whitespace at value start
|
||||
let startWithSpace = false;
|
||||
let startEnd;
|
||||
let startNlPos = -1;
|
||||
for (startEnd = 0; startEnd < value.length; ++startEnd) {
|
||||
const ch = value[startEnd];
|
||||
if (ch === ' ')
|
||||
startWithSpace = true;
|
||||
else if (ch === '\n')
|
||||
startNlPos = startEnd;
|
||||
else
|
||||
break;
|
||||
}
|
||||
let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd);
|
||||
if (start) {
|
||||
value = value.substring(start.length);
|
||||
start = start.replace(/\n+/g, `$&${indent}`);
|
||||
}
|
||||
const indentSize = indent ? '2' : '1'; // root is at -1
|
||||
// Leading | or > is added later
|
||||
let header = (startWithSpace ? indentSize : '') + chomp;
|
||||
if (comment) {
|
||||
header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
|
||||
if (onComment)
|
||||
onComment();
|
||||
}
|
||||
if (!literal) {
|
||||
const foldedValue = value
|
||||
.replace(/\n+/g, '\n$&')
|
||||
.replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded
|
||||
// ^ more-ind. ^ empty ^ capture next empty lines only at end of indent
|
||||
.replace(/\n+/g, `$&${indent}`);
|
||||
let literalFallback = false;
|
||||
const foldOptions = getFoldOptions(ctx, true);
|
||||
if (blockQuote !== 'folded' && type !== Scalar.Scalar.BLOCK_FOLDED) {
|
||||
foldOptions.onOverflow = () => {
|
||||
literalFallback = true;
|
||||
};
|
||||
}
|
||||
const body = foldFlowLines.foldFlowLines(`${start}${foldedValue}${end}`, indent, foldFlowLines.FOLD_BLOCK, foldOptions);
|
||||
if (!literalFallback)
|
||||
return `>${header}\n${indent}${body}`;
|
||||
}
|
||||
value = value.replace(/\n+/g, `$&${indent}`);
|
||||
return `|${header}\n${indent}${start}${value}${end}`;
|
||||
}
|
||||
function plainString(item, ctx, onComment, onChompKeep) {
|
||||
const { type, value } = item;
|
||||
const { actualString, implicitKey, indent, indentStep, inFlow } = ctx;
|
||||
if ((implicitKey && value.includes('\n')) ||
|
||||
(inFlow && /[[\]{},]/.test(value))) {
|
||||
return quotedString(value, ctx);
|
||||
}
|
||||
if (/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
|
||||
// not allowed:
|
||||
// - '-' or '?'
|
||||
// - start with an indicator character (except [?:-]) or /[?-] /
|
||||
// - '\n ', ': ' or ' \n' anywhere
|
||||
// - '#' not preceded by a non-space char
|
||||
// - end with ' ' or ':'
|
||||
return implicitKey || inFlow || !value.includes('\n')
|
||||
? quotedString(value, ctx)
|
||||
: blockString(item, ctx, onComment, onChompKeep);
|
||||
}
|
||||
if (!implicitKey &&
|
||||
!inFlow &&
|
||||
type !== Scalar.Scalar.PLAIN &&
|
||||
value.includes('\n')) {
|
||||
// Where allowed & type not set explicitly, prefer block style for multiline strings
|
||||
return blockString(item, ctx, onComment, onChompKeep);
|
||||
}
|
||||
if (containsDocumentMarker(value)) {
|
||||
if (indent === '') {
|
||||
ctx.forceBlockIndent = true;
|
||||
return blockString(item, ctx, onComment, onChompKeep);
|
||||
}
|
||||
else if (implicitKey && indent === indentStep) {
|
||||
return quotedString(value, ctx);
|
||||
}
|
||||
}
|
||||
const str = value.replace(/\n+/g, `$&\n${indent}`);
|
||||
// Verify that output will be parsed as a string, as e.g. plain numbers and
|
||||
// booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
|
||||
// and others in v1.1.
|
||||
if (actualString) {
|
||||
const test = (tag) => tag.default && tag.tag !== 'tag:yaml.org,2002:str' && tag.test?.test(str);
|
||||
const { compat, tags } = ctx.doc.schema;
|
||||
if (tags.some(test) || compat?.some(test))
|
||||
return quotedString(value, ctx);
|
||||
}
|
||||
return implicitKey
|
||||
? str
|
||||
: foldFlowLines.foldFlowLines(str, indent, foldFlowLines.FOLD_FLOW, getFoldOptions(ctx, false));
|
||||
}
|
||||
function stringifyString(item, ctx, onComment, onChompKeep) {
|
||||
const { implicitKey, inFlow } = ctx;
|
||||
const ss = typeof item.value === 'string'
|
||||
? item
|
||||
: Object.assign({}, item, { value: String(item.value) });
|
||||
let { type } = item;
|
||||
if (type !== Scalar.Scalar.QUOTE_DOUBLE) {
|
||||
// force double quotes on control characters & unpaired surrogates
|
||||
if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value))
|
||||
type = Scalar.Scalar.QUOTE_DOUBLE;
|
||||
}
|
||||
const _stringify = (_type) => {
|
||||
switch (_type) {
|
||||
case Scalar.Scalar.BLOCK_FOLDED:
|
||||
case Scalar.Scalar.BLOCK_LITERAL:
|
||||
return implicitKey || inFlow
|
||||
? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
|
||||
: blockString(ss, ctx, onComment, onChompKeep);
|
||||
case Scalar.Scalar.QUOTE_DOUBLE:
|
||||
return doubleQuotedString(ss.value, ctx);
|
||||
case Scalar.Scalar.QUOTE_SINGLE:
|
||||
return singleQuotedString(ss.value, ctx);
|
||||
case Scalar.Scalar.PLAIN:
|
||||
return plainString(ss, ctx, onComment, onChompKeep);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
let res = _stringify(type);
|
||||
if (res === null) {
|
||||
const { defaultKeyType, defaultStringType } = ctx.options;
|
||||
const t = (implicitKey && defaultKeyType) || defaultStringType;
|
||||
res = _stringify(t);
|
||||
if (res === null)
|
||||
throw new Error(`Unsupported default string type ${t}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
exports.stringifyString = stringifyString;
|
||||
134
bundled-addons/pithos/vendor/yaml/lib/test-events.js
vendored
Normal file
134
bundled-addons/pithos/vendor/yaml/lib/test-events.js
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('./nodes/identity.js');
|
||||
var publicApi = require('./public-api.js');
|
||||
var visit = require('./visit.js');
|
||||
|
||||
const scalarChar = {
|
||||
BLOCK_FOLDED: '>',
|
||||
BLOCK_LITERAL: '|',
|
||||
PLAIN: ':',
|
||||
QUOTE_DOUBLE: '"',
|
||||
QUOTE_SINGLE: "'"
|
||||
};
|
||||
function anchorExists(doc, anchor) {
|
||||
let found = false;
|
||||
visit.visit(doc, {
|
||||
Value(_key, node) {
|
||||
if (node.anchor === anchor) {
|
||||
found = true;
|
||||
return visit.visit.BREAK;
|
||||
}
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
// test harness for yaml-test-suite event tests
|
||||
function testEvents(src) {
|
||||
const docs = publicApi.parseAllDocuments(src);
|
||||
const errDoc = docs.find(doc => doc.errors.length > 0);
|
||||
const error = errDoc ? errDoc.errors[0].message : null;
|
||||
const events = ['+STR'];
|
||||
try {
|
||||
for (let i = 0; i < docs.length; ++i) {
|
||||
const doc = docs[i];
|
||||
let root = doc.contents;
|
||||
if (Array.isArray(root))
|
||||
root = root[0];
|
||||
const [rootStart] = doc.range || [0];
|
||||
const error = doc.errors[0];
|
||||
if (error && (!error.pos || error.pos[0] < rootStart))
|
||||
throw new Error();
|
||||
let docStart = '+DOC';
|
||||
if (doc.directives.docStart)
|
||||
docStart += ' ---';
|
||||
else if (doc.contents &&
|
||||
doc.contents.range[2] === doc.contents.range[0] &&
|
||||
!doc.contents.anchor &&
|
||||
!doc.contents.tag)
|
||||
continue;
|
||||
events.push(docStart);
|
||||
addEvents(events, doc, error?.pos[0] ?? -1, root);
|
||||
let docEnd = '-DOC';
|
||||
if (doc.directives.docEnd)
|
||||
docEnd += ' ...';
|
||||
events.push(docEnd);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
return { events, error: error ?? e };
|
||||
}
|
||||
events.push('-STR');
|
||||
return { events, error };
|
||||
}
|
||||
function addEvents(events, doc, errPos, node) {
|
||||
if (!node) {
|
||||
events.push('=VAL :');
|
||||
return;
|
||||
}
|
||||
if (errPos !== -1 && identity.isNode(node) && node.range[0] >= errPos)
|
||||
throw new Error();
|
||||
let props = '';
|
||||
let anchor = identity.isScalar(node) || identity.isCollection(node) ? node.anchor : undefined;
|
||||
if (anchor) {
|
||||
if (/\d$/.test(anchor)) {
|
||||
const alt = anchor.replace(/\d$/, '');
|
||||
if (anchorExists(doc, alt))
|
||||
anchor = alt;
|
||||
}
|
||||
props = ` &${anchor}`;
|
||||
}
|
||||
if (identity.isNode(node) && node.tag)
|
||||
props += ` <${node.tag}>`;
|
||||
if (identity.isMap(node)) {
|
||||
const ev = node.flow ? '+MAP {}' : '+MAP';
|
||||
events.push(`${ev}${props}`);
|
||||
node.items.forEach(({ key, value }) => {
|
||||
addEvents(events, doc, errPos, key);
|
||||
addEvents(events, doc, errPos, value);
|
||||
});
|
||||
events.push('-MAP');
|
||||
}
|
||||
else if (identity.isSeq(node)) {
|
||||
const ev = node.flow ? '+SEQ []' : '+SEQ';
|
||||
events.push(`${ev}${props}`);
|
||||
node.items.forEach(item => {
|
||||
addEvents(events, doc, errPos, item);
|
||||
});
|
||||
events.push('-SEQ');
|
||||
}
|
||||
else if (identity.isPair(node)) {
|
||||
events.push(`+MAP${props}`);
|
||||
addEvents(events, doc, errPos, node.key);
|
||||
addEvents(events, doc, errPos, node.value);
|
||||
events.push('-MAP');
|
||||
}
|
||||
else if (identity.isAlias(node)) {
|
||||
let alias = node.source;
|
||||
if (alias && /\d$/.test(alias)) {
|
||||
const alt = alias.replace(/\d$/, '');
|
||||
if (anchorExists(doc, alt))
|
||||
alias = alt;
|
||||
}
|
||||
events.push(`=ALI${props} *${alias}`);
|
||||
}
|
||||
else {
|
||||
const scalar = scalarChar[String(node.type)];
|
||||
if (!scalar)
|
||||
throw new Error(`Unexpected node type ${node.type}`);
|
||||
const value = node.source
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\0/g, '\\0')
|
||||
.replace(/\x07/g, '\\a')
|
||||
.replace(/\x08/g, '\\b')
|
||||
.replace(/\t/g, '\\t')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\v/g, '\\v')
|
||||
.replace(/\f/g, '\\f')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\x1b/g, '\\e');
|
||||
events.push(`=VAL${props} ${scalar}${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.testEvents = testEvents;
|
||||
28
bundled-addons/pithos/vendor/yaml/lib/util.js
vendored
Normal file
28
bundled-addons/pithos/vendor/yaml/lib/util.js
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
var createNode = require('./doc/createNode.js');
|
||||
var log = require('./log.js');
|
||||
var Pair = require('./nodes/Pair.js');
|
||||
var toJS = require('./nodes/toJS.js');
|
||||
var YAMLMap = require('./nodes/YAMLMap.js');
|
||||
var map = require('./schema/common/map.js');
|
||||
var seq = require('./schema/common/seq.js');
|
||||
var string = require('./schema/common/string.js');
|
||||
var foldFlowLines = require('./stringify/foldFlowLines.js');
|
||||
var stringifyNumber = require('./stringify/stringifyNumber.js');
|
||||
var stringifyString = require('./stringify/stringifyString.js');
|
||||
|
||||
|
||||
|
||||
exports.createNode = createNode.createNode;
|
||||
exports.debug = log.debug;
|
||||
exports.warn = log.warn;
|
||||
exports.createPair = Pair.createPair;
|
||||
exports.toJS = toJS.toJS;
|
||||
exports.findPair = YAMLMap.findPair;
|
||||
exports.mapTag = map.map;
|
||||
exports.seqTag = seq.seq;
|
||||
exports.stringTag = string.string;
|
||||
exports.foldFlowLines = foldFlowLines.foldFlowLines;
|
||||
exports.stringifyNumber = stringifyNumber.stringifyNumber;
|
||||
exports.stringifyString = stringifyString.stringifyString;
|
||||
236
bundled-addons/pithos/vendor/yaml/lib/visit.js
vendored
Normal file
236
bundled-addons/pithos/vendor/yaml/lib/visit.js
vendored
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
'use strict';
|
||||
|
||||
var identity = require('./nodes/identity.js');
|
||||
|
||||
const BREAK = Symbol('break visit');
|
||||
const SKIP = Symbol('skip children');
|
||||
const REMOVE = Symbol('remove node');
|
||||
/**
|
||||
* Apply a visitor to an AST node or document.
|
||||
*
|
||||
* Walks through the tree (depth-first) starting from `node`, calling a
|
||||
* `visitor` function with three arguments:
|
||||
* - `key`: For sequence values and map `Pair`, the node's index in the
|
||||
* collection. Within a `Pair`, `'key'` or `'value'`, correspondingly.
|
||||
* `null` for the root node.
|
||||
* - `node`: The current node.
|
||||
* - `path`: The ancestry of the current node.
|
||||
*
|
||||
* The return value of the visitor may be used to control the traversal:
|
||||
* - `undefined` (default): Do nothing and continue
|
||||
* - `visit.SKIP`: Do not visit the children of this node, continue with next
|
||||
* sibling
|
||||
* - `visit.BREAK`: Terminate traversal completely
|
||||
* - `visit.REMOVE`: Remove the current node, then continue with the next one
|
||||
* - `Node`: Replace the current node, then continue by visiting it
|
||||
* - `number`: While iterating the items of a sequence or map, set the index
|
||||
* of the next step. This is useful especially if the index of the current
|
||||
* node has changed.
|
||||
*
|
||||
* If `visitor` is a single function, it will be called with all values
|
||||
* encountered in the tree, including e.g. `null` values. Alternatively,
|
||||
* separate visitor functions may be defined for each `Map`, `Pair`, `Seq`,
|
||||
* `Alias` and `Scalar` node. To define the same visitor function for more than
|
||||
* one node type, use the `Collection` (map and seq), `Value` (map, seq & scalar)
|
||||
* and `Node` (alias, map, seq & scalar) targets. Of all these, only the most
|
||||
* specific defined one will be used for each node.
|
||||
*/
|
||||
function visit(node, visitor) {
|
||||
const visitor_ = initVisitor(visitor);
|
||||
if (identity.isDocument(node)) {
|
||||
const cd = visit_(null, node.contents, visitor_, Object.freeze([node]));
|
||||
if (cd === REMOVE)
|
||||
node.contents = null;
|
||||
}
|
||||
else
|
||||
visit_(null, node, visitor_, Object.freeze([]));
|
||||
}
|
||||
// Without the `as symbol` casts, TS declares these in the `visit`
|
||||
// namespace using `var`, but then complains about that because
|
||||
// `unique symbol` must be `const`.
|
||||
/** Terminate visit traversal completely */
|
||||
visit.BREAK = BREAK;
|
||||
/** Do not visit the children of the current node */
|
||||
visit.SKIP = SKIP;
|
||||
/** Remove the current node */
|
||||
visit.REMOVE = REMOVE;
|
||||
function visit_(key, node, visitor, path) {
|
||||
const ctrl = callVisitor(key, node, visitor, path);
|
||||
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
||||
replaceNode(key, path, ctrl);
|
||||
return visit_(key, ctrl, visitor, path);
|
||||
}
|
||||
if (typeof ctrl !== 'symbol') {
|
||||
if (identity.isCollection(node)) {
|
||||
path = Object.freeze(path.concat(node));
|
||||
for (let i = 0; i < node.items.length; ++i) {
|
||||
const ci = visit_(i, node.items[i], visitor, path);
|
||||
if (typeof ci === 'number')
|
||||
i = ci - 1;
|
||||
else if (ci === BREAK)
|
||||
return BREAK;
|
||||
else if (ci === REMOVE) {
|
||||
node.items.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (identity.isPair(node)) {
|
||||
path = Object.freeze(path.concat(node));
|
||||
const ck = visit_('key', node.key, visitor, path);
|
||||
if (ck === BREAK)
|
||||
return BREAK;
|
||||
else if (ck === REMOVE)
|
||||
node.key = null;
|
||||
const cv = visit_('value', node.value, visitor, path);
|
||||
if (cv === BREAK)
|
||||
return BREAK;
|
||||
else if (cv === REMOVE)
|
||||
node.value = null;
|
||||
}
|
||||
}
|
||||
return ctrl;
|
||||
}
|
||||
/**
|
||||
* Apply an async visitor to an AST node or document.
|
||||
*
|
||||
* Walks through the tree (depth-first) starting from `node`, calling a
|
||||
* `visitor` function with three arguments:
|
||||
* - `key`: For sequence values and map `Pair`, the node's index in the
|
||||
* collection. Within a `Pair`, `'key'` or `'value'`, correspondingly.
|
||||
* `null` for the root node.
|
||||
* - `node`: The current node.
|
||||
* - `path`: The ancestry of the current node.
|
||||
*
|
||||
* The return value of the visitor may be used to control the traversal:
|
||||
* - `Promise`: Must resolve to one of the following values
|
||||
* - `undefined` (default): Do nothing and continue
|
||||
* - `visit.SKIP`: Do not visit the children of this node, continue with next
|
||||
* sibling
|
||||
* - `visit.BREAK`: Terminate traversal completely
|
||||
* - `visit.REMOVE`: Remove the current node, then continue with the next one
|
||||
* - `Node`: Replace the current node, then continue by visiting it
|
||||
* - `number`: While iterating the items of a sequence or map, set the index
|
||||
* of the next step. This is useful especially if the index of the current
|
||||
* node has changed.
|
||||
*
|
||||
* If `visitor` is a single function, it will be called with all values
|
||||
* encountered in the tree, including e.g. `null` values. Alternatively,
|
||||
* separate visitor functions may be defined for each `Map`, `Pair`, `Seq`,
|
||||
* `Alias` and `Scalar` node. To define the same visitor function for more than
|
||||
* one node type, use the `Collection` (map and seq), `Value` (map, seq & scalar)
|
||||
* and `Node` (alias, map, seq & scalar) targets. Of all these, only the most
|
||||
* specific defined one will be used for each node.
|
||||
*/
|
||||
async function visitAsync(node, visitor) {
|
||||
const visitor_ = initVisitor(visitor);
|
||||
if (identity.isDocument(node)) {
|
||||
const cd = await visitAsync_(null, node.contents, visitor_, Object.freeze([node]));
|
||||
if (cd === REMOVE)
|
||||
node.contents = null;
|
||||
}
|
||||
else
|
||||
await visitAsync_(null, node, visitor_, Object.freeze([]));
|
||||
}
|
||||
// Without the `as symbol` casts, TS declares these in the `visit`
|
||||
// namespace using `var`, but then complains about that because
|
||||
// `unique symbol` must be `const`.
|
||||
/** Terminate visit traversal completely */
|
||||
visitAsync.BREAK = BREAK;
|
||||
/** Do not visit the children of the current node */
|
||||
visitAsync.SKIP = SKIP;
|
||||
/** Remove the current node */
|
||||
visitAsync.REMOVE = REMOVE;
|
||||
async function visitAsync_(key, node, visitor, path) {
|
||||
const ctrl = await callVisitor(key, node, visitor, path);
|
||||
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
||||
replaceNode(key, path, ctrl);
|
||||
return visitAsync_(key, ctrl, visitor, path);
|
||||
}
|
||||
if (typeof ctrl !== 'symbol') {
|
||||
if (identity.isCollection(node)) {
|
||||
path = Object.freeze(path.concat(node));
|
||||
for (let i = 0; i < node.items.length; ++i) {
|
||||
const ci = await visitAsync_(i, node.items[i], visitor, path);
|
||||
if (typeof ci === 'number')
|
||||
i = ci - 1;
|
||||
else if (ci === BREAK)
|
||||
return BREAK;
|
||||
else if (ci === REMOVE) {
|
||||
node.items.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (identity.isPair(node)) {
|
||||
path = Object.freeze(path.concat(node));
|
||||
const ck = await visitAsync_('key', node.key, visitor, path);
|
||||
if (ck === BREAK)
|
||||
return BREAK;
|
||||
else if (ck === REMOVE)
|
||||
node.key = null;
|
||||
const cv = await visitAsync_('value', node.value, visitor, path);
|
||||
if (cv === BREAK)
|
||||
return BREAK;
|
||||
else if (cv === REMOVE)
|
||||
node.value = null;
|
||||
}
|
||||
}
|
||||
return ctrl;
|
||||
}
|
||||
function initVisitor(visitor) {
|
||||
if (typeof visitor === 'object' &&
|
||||
(visitor.Collection || visitor.Node || visitor.Value)) {
|
||||
return Object.assign({
|
||||
Alias: visitor.Node,
|
||||
Map: visitor.Node,
|
||||
Scalar: visitor.Node,
|
||||
Seq: visitor.Node
|
||||
}, visitor.Value && {
|
||||
Map: visitor.Value,
|
||||
Scalar: visitor.Value,
|
||||
Seq: visitor.Value
|
||||
}, visitor.Collection && {
|
||||
Map: visitor.Collection,
|
||||
Seq: visitor.Collection
|
||||
}, visitor);
|
||||
}
|
||||
return visitor;
|
||||
}
|
||||
function callVisitor(key, node, visitor, path) {
|
||||
if (typeof visitor === 'function')
|
||||
return visitor(key, node, path);
|
||||
if (identity.isMap(node))
|
||||
return visitor.Map?.(key, node, path);
|
||||
if (identity.isSeq(node))
|
||||
return visitor.Seq?.(key, node, path);
|
||||
if (identity.isPair(node))
|
||||
return visitor.Pair?.(key, node, path);
|
||||
if (identity.isScalar(node))
|
||||
return visitor.Scalar?.(key, node, path);
|
||||
if (identity.isAlias(node))
|
||||
return visitor.Alias?.(key, node, path);
|
||||
return undefined;
|
||||
}
|
||||
function replaceNode(key, path, node) {
|
||||
const parent = path[path.length - 1];
|
||||
if (identity.isCollection(parent)) {
|
||||
parent.items[key] = node;
|
||||
}
|
||||
else if (identity.isPair(parent)) {
|
||||
if (key === 'key')
|
||||
parent.key = node;
|
||||
else
|
||||
parent.value = node;
|
||||
}
|
||||
else if (identity.isDocument(parent)) {
|
||||
parent.contents = node;
|
||||
}
|
||||
else {
|
||||
const pt = identity.isAlias(parent) ? 'alias' : 'scalar';
|
||||
throw new Error(`Cannot replace node with ${pt} parent`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.visit = visit;
|
||||
exports.visitAsync = visitAsync;
|
||||
6
bundled-addons/pithos/vendor/yaml/package.json
vendored
Normal file
6
bundled-addons/pithos/vendor/yaml/package.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "yaml",
|
||||
"version": "2.9.1",
|
||||
"type": "commonjs",
|
||||
"private": true
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue