Add base plugin logic

This commit is contained in:
Anton Duda 2025-10-23 20:54:43 +03:00
parent 2f74420965
commit 104bc3553d
181 changed files with 21479 additions and 12872 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
TARGET_VAULT_DIR="/path/to/plugin/dir"
TARGET_TEST_VAULT_DIR="/path/to/plugin/dir"

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

84
.eslintrc Normal file
View file

@ -0,0 +1,84 @@
{
"parser": "vue-eslint-parser",
"plugins": ["@typescript-eslint/eslint-plugin", "eslint-plugin-vue", "prettier"],
"extends": ["plugin:@typescript-eslint/recommended", "plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"node": true
},
"ignorePatterns": [
"docker-formatter.*",
"node_modules/*",
".nuxt/*",
"dist/*",
"dist-electron/*",
"server/*",
"scripts/*",
"src/assets/*"
],
"rules": {
"arrow-body-style": 0,
"class-methods-use-this": 0,
"comma-dangle": 0,
"func-names": 0,
"import/extensions": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default-member": 0,
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"linebreak-style": 0,
"max-len": [
"warn",
{
"code": 120,
"ignoreTemplateLiterals": true,
"ignoreComments": true,
"ignoreStrings": true
}
],
"no-await-in-loop": 0,
"no-console": 0,
"no-loop-func": 0,
"no-mixed-operators": 0,
"no-param-reassign": 0,
"no-restricted-syntax": 0,
"no-shadow": 0,
"no-trailing-spaces": 1,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"prefer-destructuring": 0,
"semi": 0,
"space-before-function-paren": 0,
"vue/multi-word-component-names": "off",
"vue/no-dupe-keys": "warn",
"vue/no-mutating-props": ["warn", { "shallowOnly": true }],
"vue/no-unused-vars": ["warn", { "ignorePattern": "^_" }],
"vue/no-use-v-if-with-v-for": "warn",
"vue/prop-name-casing": "error",
"vue/return-in-computed-property": "off",
"vue/require-prop-types": "error",
"vue/require-toggle-inside-transition": "warn",
"vue/require-typed-object-prop": "error",
"vue/require-v-for-key": "warn",
"vue/valid-v-for": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-inferrable-types": ["warn", { "ignoreParameters": true }],
"@typescript-eslint/no-unsafe-declaration-merging": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"prettier/prettier": ["warn"]
}
}

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
auto-imports.d.ts
components.d.ts
build/*
.env

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

15
.prettierrc.mjs Normal file
View file

@ -0,0 +1,15 @@
/**
* @type {import('prettier').Options}
*/
const config = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
printWidth: 100,
importOrder: ['^@/(.*)$', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
}
export default config

622
LICENSE Normal file
View file

@ -0,0 +1,622 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Strudel Obsidian plugin
TODO

41
build_and_copy_plugin.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
source ./.env
# Exit immediately if a command exits with a non-zero status.
set -e
# Define plugin source and build directories
PLUGIN_DIR="."
BUILD_DIR="$PLUGIN_DIR/build"
echo "--- Building Strudel Obsidian Plugin ---"
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
echo "Node modules not found. Installing dependencies..."
npm install
fi
# Run the build script
echo "Running build..."
npm run build
echo "--- Copying Plugin Files to Test Vault ---"
# Create target directory in test vault if it doesn't exist
mkdir -p "$TARGET_TEST_VAULT_DIR"
echo "Ensured target directory exists: $TARGET_TEST_VAULT_DIR"
# Copy built files
cp "$BUILD_DIR/main.js" "$TARGET_TEST_VAULT_DIR/main.js"
echo "Copied main.js"
cp "$BUILD_DIR/main.css" "$TARGET_TEST_VAULT_DIR/styles.css"
echo "Copied styles.css"
cp "$PLUGIN_DIR/manifest.json" "$TARGET_TEST_VAULT_DIR/manifest.json"
echo "Copied manifest.json"
echo "--- Plugin Build and Copy Complete ---"
echo "Plugin files are ready in: $TARGET_TEST_VAULT_DIR"

35
build_and_copy_plugin_prod.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Define plugin source and build directories
PLUGIN_DIR="."
BUILD_DIR="$PLUGIN_DIR/build"
echo "--- Building Strudel Obsidian Plugin ---"
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
echo "Node modules not found. Installing dependencies..."
npm install
fi
# Run the build script
echo "Running build..."
npm run build
echo "--- Copying Plugin Files to Test Vault ---"
# Copy built files
cp -X "$BUILD_DIR/main.js" "$TARGET_VAULT_DIR/main.js"
echo "Copied main.js"
cp -X "$BUILD_DIR/main.css" "$TARGET_VAULT_DIR/styles.css"
echo "Copied styles.css"
cp -X "$PLUGIN_DIR/manifest.json" "$TARGET_VAULT_DIR/manifest.json"
echo "Copied manifest.json"
echo "--- Plugin Build and Copy Complete ---"
echo "Plugin files are ready in: $TARGET_VAULT_DIR"

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"id": "strudel-repl",
"name": "Strudel REPL",
"version": "0.0.1",
"minAppVersion": "0.15.0",
"description": "",
"author": "Anton Duda",
"isDesktopOnly": false
}

6714
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

62
package.json Normal file
View file

@ -0,0 +1,62 @@
{
"name": "strudel-obsidian-plugin",
"version": "0.0.1",
"description": "Strudel REPL Obsidian plugin",
"main": "main.js",
"scripts": {
"dev": "vite preview",
"build": "tsc -noEmit -skipLibCheck && vite build",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.6.2",
"@types/node": "^24.9.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@vitejs/plugin-vue": "^5.2.4",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild-plugin-vue3": "^0.3.2",
"eslint": "8.31.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-vue": "9.23.0",
"obsidian": "latest",
"prettier": "^3.5.3",
"prettier-eslint": "16.1.2",
"sass-embedded": "^1.93.2",
"tslib": "2.4.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vue-tsc": "^2.2.10"
},
"dependencies": {
"@codemirror/language": "https://github.com/lishid/cm-language",
"@codemirror/merge": "^6.11.0",
"@internationalized/date": "^3.8.2",
"@tonaljs/tonal": "^4.10.0",
"@vueuse/core": "^13.9.0",
"acorn": "^8.15.0",
"chord-voicings": "^0.0.1",
"dayjs": "^1.11.18",
"escodegen": "^2.1.0",
"estree-walker": "^3.0.3",
"file-type": "^20.5.0",
"fraction.js": "^5.3.4",
"front-matter": "^4.0.2",
"lucide-vue-next": "^0.284.0",
"nanoid": "^5.1.6",
"nanostores": "^1.0.1",
"pinia": "^3.0.2",
"sfumato": "^0.1.2",
"soundfont2": "^0.5.0",
"uuid": "^11.1.0",
"vue": "^3.5.16",
"webmidi": "^3.1.14"
}
}

9
src/App.vue Normal file
View file

@ -0,0 +1,9 @@
<template>
<div class="strudel">
<Views />
</div>
</template>
<script setup lang="ts">
import Views from './components/Views.vue'
</script>

View file

@ -0,0 +1,28 @@
import { STRUDEL_CODEBLOCK_KEYWORD } from '@/constants/keywords'
import { Editor, Notice } from 'obsidian'
export const createStrudelBlock = async (editor: Editor) => {
if (!editor) {
new Notice('No active markdown editor found.', 3000)
return
}
const cursor = editor.getCursor()
const shouldCreateNewLine = cursor.ch > 0
const selection = editor.getSelection()
const strudelBlock = `\`\`\`${STRUDEL_CODEBLOCK_KEYWORD}
\`\`\``
if (selection) {
editor.replaceSelection(strudelBlock)
} else {
// If no selection, just insert the linkPath at the cursor position
editor.replaceRange(strudelBlock, cursor)
}
// setting cursor to the line inside the new strudel block
editor.setSelection({ line: cursor.line + (shouldCreateNewLine ? 2 : 1), ch: 0 })
}

View file

@ -0,0 +1,120 @@
<template>
<div class="strudel-block">
<div class="strudel-block__header">
<ObsidianIcon
:icon="isCurrentBlockPlaying ? 'circle-stop' : 'play'"
@click="isCurrentBlockPlaying ? stop() : play()"
/>
<ObsidianIcon v-if="isCurrentBlockPlaying" icon="refresh-ccw" @click="play()" />
</div>
<canvas v-show="isCurrentBlockPlaying" ref="canvas"></canvas>
<!-- <div ref="editorRef"></div> -->
</div>
</template>
<script setup lang="ts">
import ObsidianIcon from './obsidian/Icon.vue'
import { Strudel } from '@/entities/Strudel'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { initStrudel } from '@/strudel/init.js'
import { GlobalStore } from '@/stores/GlobalStore'
import { updateMiniLocations } from '@/editor/StrudelHighlight'
import { MarkdownView, WorkspaceLeaf } from 'obsidian'
import { EditorView } from '@codemirror/view'
const props = defineProps<{
strudelBlock: Strudel
}>()
const editorRef = ref<HTMLElement | null>(null)
const { isPlaying, currentBlock } = GlobalStore.getInstance()
const isCurrentBlockPlaying = computed(() => {
return isPlaying.value && currentBlock.value?.id === props.strudelBlock.id
})
watch(
() => props.strudelBlock,
(newBlock) => {
if (isCurrentBlockPlaying.value) {
console.log(GlobalStore.getInstance().repl)
// GlobalStore.getInstance().repl.setCode(newBlock.code)
// GlobalStore.getInstance().resetMiniLocations()
}
},
{ deep: true }
)
const canvas = ref<HTMLCanvasElement | null>(null)
//
// const editor = new StrudelMirror({
// defaultOutput: webaudioOutput,
// getTime: () => getAudioContext().currentTime,
// transpiler,
// root: document.getElementById('editor'),
// initialCode: funk42,
// drawTime,
// onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
// prebake: async () => {
// initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
// const loadModules = evalScope(
// import('@strudel/core'),
// import('@strudel/draw'),
// import('@strudel/mini'),
// import('@strudel/tonal'),
// import('@strudel/webaudio'),
// );
// await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
// },
// });
//
// document.getElementById('play').addEventListener('click', () => editor.evaluate());
// document.getElementById('stop').addEventListener('click', () => editor.stop());
const test = () => {
const activeLeaf = GlobalStore.getInstance()
.app.workspace.getLeavesOfType('markdown')
.find(
(leaf: WorkspaceLeaf) =>
(leaf.view as MarkdownView).file?.path === props.strudelBlock.filePath
)
const view = activeLeaf?.view as MarkdownView
if (activeLeaf && view.editor) {
console.log('found editor for strudel block')
// updateMiniLocations((view.editor as any).cm as EditorView, [])
}
}
const play = () => {
GlobalStore.getInstance().play(props.strudelBlock)
}
const stop = () => {
GlobalStore.getInstance().stop()
}
onMounted(() => {
const drawContext = canvas.value.getContext('2d')
props.strudelBlock.setDrawContext(drawContext)
})
onUnmounted(() => {
stop()
})
</script>
<style>
.strudel-block .strudel-block__header {
display: flex;
align-items: center;
}
.strudel-block canvas {
width: var(--file-line-width);
height: 80px;
}
</style>

13
src/components/Views.vue Normal file
View file

@ -0,0 +1,13 @@
<template>
<Teleport v-for="block in strudelBlocks" :key="block.id" :to="`[data-strudel-id='${block.id}']`">
<StrudelBlockView :strudel-block="block as Strudel" />
</Teleport>
</template>
<script setup lang="ts">
import { GlobalStore } from '@/stores/GlobalStore'
import { Strudel } from '@/entities/Strudel'
import StrudelBlockView from './StrudelBlockView.vue'
const { strudelBlocks } = GlobalStore.getInstance()
</script>

View file

@ -0,0 +1,92 @@
<template>
<div
class="strudel-obsidian-icon"
:class="{
'strudel-obsidian-icon_with-bg': withBg,
'strudel-obsidian-icon_no-hover': noHover,
}"
@click="emit('click', $event)"
>
<div v-if="textLeft" class="strudel-obsidian-icon__text">{{ textLeft }}</div>
<div v-if="icon" ref="iconEl" class="strudel-obsidian-icon__icon" />
<div v-if="textRight" class="strudel-obsidian-icon__text">{{ textRight }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { setIcon } from 'obsidian'
const props = defineProps<{
icon?: string
textLeft?: string
textRight?: string
withBg?: boolean
noHover?: boolean
}>()
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const iconEl = ref<HTMLElement>()
const updateIcon = () => {
if (iconEl.value) {
iconEl.value.empty()
if (props.icon) {
setIcon(iconEl.value, props.icon)
}
}
}
onMounted(updateIcon)
watch(() => props.icon, updateIcon)
</script>
<style lang="scss">
.strudel-obsidian-icon {
display: flex;
align-items: center;
justify-content: center;
height: 1.5em;
color: var(--icon-color);
padding: var(--size-2-1) var(--size-2-2);
border-radius: var(--radius-s);
&_with-bg {
background-color: var(--background-secondary);
}
&:hover {
cursor: var(--cursor-link);
color: var(--text-normal);
background-color: var(--background-modifier-hover);
}
&_no-hover {
&:hover {
cursor: default;
color: var(--icon-color);
background-color: transparent;
}
}
}
.strudel-obsidian-icon__icon {
display: flex;
align-items: center;
height: 1.5em;
}
.strudel-obsidian-icon__text {
user-select: none;
font-size: var(--font-smaller);
&:first-child {
margin-right: var(--size-2-2);
}
&:last-child {
margin-left: var(--size-2-2);
}
}
</style>

View file

@ -0,0 +1 @@
export const STRUDEL_CODEBLOCK_KEYWORD = 'strudel'

View file

@ -0,0 +1,58 @@
import { WidgetType } from '@codemirror/view'
import { genid } from '@/helpers/vueUtils'
import { GlobalStore } from '@/stores/GlobalStore'
import { Strudel } from '@/entities/Strudel'
export class StrudelHeaderWidget extends WidgetType {
private instance: Strudel
constructor(code: string, filePath: string, lineFrom: number) {
super()
this.instance = new Strudel({
id: genid(),
code,
filePath,
lineFrom,
})
}
getInstance() {
return this.instance
}
// should be called before any DOM updates
updateInstance(newInstance: Strudel) {
this.instance = newInstance
}
toDOM() {
const container = document.createElement('div')
container.id = this.instance.id
container.createDiv({ attr: { 'data-strudel-id': this.instance.id } })
if (!GlobalStore.getInstance().strudelBlocks.value.find((t) => t.id === this.instance.id)) {
GlobalStore.getInstance().strudelBlocks.value.push(this.instance)
}
return container
}
destroy() {
const store = GlobalStore.getInstance()
const index = store.strudelBlocks.value.findIndex((t) => t.id === this.instance.id)
if (index !== -1) {
// store.strudelBlocks.value[index].cleanup()
store.strudelBlocks.value.splice(index, 1)
}
}
eq(other: StrudelHeaderWidget) {
return this.instance.compare(other.instance)
}
ignoreEvent() {
return true
}
}

View file

@ -0,0 +1,164 @@
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'
import { Decoration, DecorationSet, EditorView } from '@codemirror/view'
export const setMiniLocations = StateEffect.define<{
locations: Array<[number, number]>
shift: number
}>()
export const showMiniLocations = StateEffect.define<{
atTime: number
haps: any[]
}>()
export const updateMiniLocations = (
view: EditorView,
locations: Array<[number, number]>,
shift: number
) => {
view.dispatch({ effects: setMiniLocations.of({ locations, shift }) })
}
export const highlightMiniLocations = (view: EditorView, atTime: number, haps: any[]) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) })
}
const miniLocations = StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(locations, tr) {
if (tr.docChanged) {
locations = locations.map(tr.changes)
}
// console.log('miniLocations update', tr.effects)
for (const e of tr.effects) {
if (e.is(setMiniLocations)) {
// this is called on eval, with the mini locations obtained from the transpiler
// codemirror will automatically remap the marks when the document is edited
// create a mark for each mini location, adding the range to the spec to find it later
const marks = e.value.locations
.filter(([from]) => from < tr.newDoc.length)
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
.map(
(range) =>
Decoration.mark({
id: range.join(':'),
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
// attributes: { style: `background-color: #00CA2880` },
}).range(range[0] + e.value.shift, range[1] + e.value.shift) // -> Decoration
)
locations = Decoration.set(marks, true) // -> DecorationSet === RangeSet<Decoration>
}
}
return locations
},
})
const visibleMiniLocations = StateField.define<{
atTime: number
haps: Map<string, any>
}>({
create() {
return { atTime: 0, haps: new Map(), shift: 0 }
},
update(visible, tr) {
for (const e of tr.effects) {
if (e.is(showMiniLocations)) {
// this is called every frame to show the locations that are currently active
// we can NOT create new marks because the context.locations haven't changed since eval time
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map()
// console.log(e.value, e.value.haps)
for (const hap of e.value.haps) {
if (!hap.context?.locations || !hap.whole) {
continue
}
for (const { start, end } of hap.context.locations) {
// start += e.value.shift
// end += e.value.shift
const id = `${start}:${end}`
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
haps.set(id, hap)
}
}
}
visible = { atTime: e.value.atTime, haps }
}
}
return visible
},
})
// // Derive the set of decorations from the miniLocations and visibleLocations
const miniLocationHighlights = EditorView.decorations.compute(
[miniLocations, visibleMiniLocations],
(state) => {
const iterator = state.field(miniLocations).iter()
const { haps } = state.field(visibleMiniLocations)
const builder = new RangeSetBuilder<Decoration>()
// console.log('highlighting mini locations', haps, shift)
while (iterator.value) {
const {
from,
to,
value: {
spec: { id },
},
} = iterator
// console.log(haps, id)
if (haps.has(id)) {
const hap = haps.get(id)
const colorMod = hap.value?.color ? ` strudel-mark_${hap.value.color}` : ''
// const style = hap.value?.markcss || `outline: solid 2px ${color}`
const classStr = `strudel-mark${colorMod}`
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
let channels = getComputedStyle(swatch)
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
.slice(1)
.map((c) => parseFloat(c || 1));
document.body.removeChild(swatch);
// Get percentage of event
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
channels[3] *= percent;
*/
builder.add(
from,
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { class: classStr },
})
)
}
iterator.next()
}
return builder.finish()
}
)
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]
// export const isPatternHighlightingEnabled = (on, config) => {
// on &&
// config &&
// setTimeout(() => {
// updateMiniLocations(config.editor, config.miniLocations)
// }, 100)
// return on ? Prec.highest(highlightExtension) : []
// }

View file

@ -0,0 +1,97 @@
import { StateField, RangeSetBuilder, EditorState } from '@codemirror/state'
import { Decoration, DecorationSet, EditorView } from '@codemirror/view'
import { editorLivePreviewField, editorInfoField } from 'obsidian'
import { StrudelHeaderWidget } from './StrudelHeaderWidget'
import { STRUDEL_CODEBLOCK_KEYWORD } from '@/constants/keywords'
import { GlobalStore } from '@/stores/GlobalStore'
function buildStrudelDecorations(state: EditorState): DecorationSet {
const builder = new RangeSetBuilder<Decoration>()
if (!state.field(editorLivePreviewField)) {
return builder.finish()
}
const currentFile = state.field(editorInfoField)?.file
if (!currentFile) {
return builder.finish()
}
for (let i = 1; i <= state.doc.lines; i++) {
const line = state.doc.line(i)
if (line.text.trim() === `\`\`\`${STRUDEL_CODEBLOCK_KEYWORD}`) {
const codeLines = []
let endLineFound = false
// find the end of the code block and collect code lines
let endLineNumber = i + 1
while (endLineNumber <= state.doc.lines) {
const endLine = state.doc.line(endLineNumber)
if (endLine.text.trim() === '```') {
endLineFound = true
break
}
codeLines.push(endLine.text)
endLineNumber++
}
if (!endLineFound) {
continue // skip if no closing ```
}
const code = codeLines.join('\n')
if (!code.trim()) {
continue // skip empty code blocks
}
const widget = new StrudelHeaderWidget(code, currentFile.path, state.doc.line(i + 1).from)
// searching for existing instance
const existingInstance = GlobalStore.getInstance().strudelBlocks.value.find((s) =>
s.compare(widget.getInstance())
)
if (existingInstance) {
existingInstance.code = code
existingInstance.lineFrom = state.doc.line(i + 1).from
// update instance at newly created widget
widget.updateInstance(existingInstance)
}
// add widget at the start of the code block
builder.add(
line.from,
line.from,
Decoration.widget({
widget,
block: true,
side: -1,
})
)
}
}
return builder.finish()
}
export const strudelStateField = StateField.define<DecorationSet>({
create(state) {
return buildStrudelDecorations(state)
},
update(decorations, tr) {
if (
tr.docChanged ||
tr.selection ||
tr.state.field(editorLivePreviewField) !== tr.startState.field(editorLivePreviewField)
) {
return buildStrudelDecorations(tr.state)
}
return decorations.map(tr.changes)
},
provide(field) {
return [EditorView.decorations.from(field)]
},
})

File diff suppressed because it is too large Load diff

35
src/entities/Strudel.ts Normal file
View file

@ -0,0 +1,35 @@
export class Strudel {
public id: string
public code: string
public filePath: string
public lineFrom: number
public drawContext: CanvasRenderingContext2D | null = null
public lineFromWhenPlaybackStarts: number | null = null
constructor(data: { id: string; code: string; filePath: string; lineFrom: number }) {
this.id = data.id
this.code = data.code
this.filePath = data.filePath
this.lineFrom = data.lineFrom
}
compare(other: Strudel) {
return (
(this.code === other.code || this.lineFrom === other.lineFrom) &&
this.filePath === other.filePath
)
}
setDrawContext(ctx: CanvasRenderingContext2D) {
this.drawContext = ctx
}
playbackStarted() {
this.lineFromWhenPlaybackStarts = this.lineFrom
}
playbackStopped() {
this.lineFromWhenPlaybackStarts = null
}
}

3
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
declare module '@/strudel/init.js'
declare module '@/strudel/codemirror/highlight.mjs'
declare module '@/strudel/draw/index.mjs'

116
src/helpers/vueUtils.ts Normal file
View file

@ -0,0 +1,116 @@
import { nanoid } from 'nanoid'
import { App } from 'obsidian'
import { createPinia } from 'pinia'
import { createApp, defineComponent, Component as VueComponent } from 'vue'
/**
* Helper class for managing Vue components in Obsidian
*/
export class VueRenderer {
private static instance: VueRenderer
private app: App
private vueApps: Map<string, any> = new Map()
private constructor(app: App) {
this.app = app
}
/**
* Get the singleton instance of VueRenderer
* @param app Obsidian App instance
* @returns VueRenderer instance
*/
public static getInstance(app: App): VueRenderer {
if (!VueRenderer.instance) {
VueRenderer.instance = new VueRenderer(app)
}
return VueRenderer.instance
}
/**
* Mount a Vue component to a DOM element
* @param container The DOM element to mount the component to
* @param component The Vue component to mount
* @param props Props to pass to the component
* @returns A unique ID for the mounted component
*/
public mountComponent(
container: HTMLElement,
component: VueComponent,
props: Record<string, any> = {}
): string {
// Create a unique ID for this component instance
const id = `vue-component-${Date.now()}-${Math.floor(Math.random() * 10000)}`
// Create a wrapper div for the Vue app
const mountPoint = document.createElement('div')
mountPoint.id = id
container.appendChild(mountPoint)
const pinia = createPinia()
// Create a Vue app with the component
const app = createApp(component, props)
app.use(pinia)
// Mount the app to the container
app.mount(`#${id}`)
// Store the app reference for cleanup
this.vueApps.set(id, app)
return id
}
/**
* Unmount a Vue component by its ID
* @param id The ID of the component to unmount
*/
public unmountComponent(id: string): void {
const app = this.vueApps.get(id)
if (app) {
app.unmount()
this.vueApps.delete(id)
const mountPoint = document.getElementById(id)
if (mountPoint) {
mountPoint.remove()
}
}
}
/**
* Create a wrapper for a Vue component that can be used with Obsidian's Component system
* @param component The Vue component to wrap
* @param props Props to pass to the component
* @returns A function that can be used to mount the component
*/
public createComponentWrapper(
component: VueComponent,
props: Record<string, any> = {}
): (container: HTMLElement) => string {
return (container: HTMLElement) => {
return this.mountComponent(container, component, props)
}
}
}
/**
* Create a simple Vue component from a template string
* @param template The Vue template string
* @param options Additional Vue component options
* @returns A Vue component
*/
export function createVueComponent(
template: string,
options: Record<string, any> = {}
): VueComponent {
return defineComponent({
template,
...options,
})
}
export function genid(): string {
return `vue-${nanoid()}`
}

119
src/main.ts Normal file
View file

@ -0,0 +1,119 @@
import { MarkdownPostProcessorContext, Plugin } from 'obsidian'
import './styles.css'
import { GlobalStore } from './stores/GlobalStore'
import { createApp, App as VueApp } from 'vue'
import { createPinia } from 'pinia'
import VueEntry from './App.vue'
import { StrudelConfig } from './services/StrudelConfig'
import { strudelStateField } from './editor/StrudelPlugin'
import { highlightExtension } from './editor/StrudelHighlight'
// import { STRUDEL_CODEBLOCK_KEYWORD } from './constants/keywords'
// import { StrudelRenderer } from './editor/StrudelRenderer'
// import { highlightExtension } from '@/strudel/codemirror/highlight.mjs'
import './editor/SyntaxHighlighting.js'
import { createStrudelBlock } from './commands/createStrudelBlock'
export interface StrudelSettings {}
const DEFAULT_SETTINGS: StrudelSettings = {}
interface PluginData {}
export default class StrudelPlugin extends Plugin {
settings: StrudelSettings
strudelConfig: StrudelConfig
private data: PluginData = {}
private vueApp: VueApp | null = null
initializeVue() {
const rootContainer = document.createElement('div')
rootContainer.id = 'strudel-vue-root'
rootContainer.style.display = 'none'
document.body.appendChild(rootContainer)
this.vueApp = createApp(VueEntry)
this.vueApp.use(createPinia())
this.vueApp.mount(rootContainer)
}
getVueApp(): VueApp {
return this.vueApp!
}
async onload() {
await this.loadPluginData()
await this.loadSettings()
GlobalStore.getInstance().init(this.app)
// this.addSettingTab(new StrudelSettingTab(this.app, this))
console.log('Strudel REPL Plugin loaded.')
this.initializeVue()
this.registerEditorExtension(strudelStateField)
this.registerEditorExtension(highlightExtension)
GlobalStore.getInstance().initStrudel()
// this.addCommand({
// id: 'create-strudel-block',
// name: 'Create new strudel block',
// callback: () => {
// createStrudelBlock()
// },
// })
}
onunload() {
GlobalStore.getInstance().destroy()
if (this.vueApp) {
this.vueApp.unmount()
document.getElementById('strudel-vue-root')?.remove()
}
console.log('Strudel REPL plugin unloaded.')
}
applySettings() {
this.strudelConfig = StrudelConfig.getInstance()
// this.strudelConfig.refreshDelay = this.settings.refreshDelay
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
this.applySettings()
}
async saveSettings() {
await this.saveData(this.settings)
this.applySettings()
}
async loadPluginData() {
this.data = (await this.loadData()) || {}
}
async savePluginData() {
await this.saveData(this.data)
}
}
// class StrudelSettingTab extends PluginSettingTab {
// plugin: StrudelPlugin
//
// constructor(app: App, plugin: StrudelPlugin) {
// super(app, plugin)
// this.plugin = plugin
// }
//
// display(): void {
// const { containerEl } = this
//
// containerEl.empty()
//
// containerEl.createEl('h2', { text: 'Strudel REPL Settings' })
// }
// }

View file

@ -0,0 +1,13 @@
export class StrudelConfig {
private static instance: StrudelConfig
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static getInstance(): StrudelConfig {
if (!StrudelConfig.instance) {
StrudelConfig.instance = new StrudelConfig()
}
return StrudelConfig.instance
}
}

189
src/stores/GlobalStore.ts Normal file
View file

@ -0,0 +1,189 @@
import { Strudel } from '@/entities/Strudel'
import { App } from 'obsidian'
import { ref } from 'vue'
import { EditorView } from '@codemirror/view'
import { MarkdownView, WorkspaceLeaf } from 'obsidian'
import { initStrudel as init, recalculateMiniLocations } from '@/strudel/init.js'
import { highlightMiniLocations, updateMiniLocations } from '@/editor/StrudelHighlight'
import { Drawer, drawPianoroll } from '@/strudel/draw/index.mjs'
export class GlobalStore {
private static instance: GlobalStore
private _app: App
public repl: any
public readonly strudelInitialized = ref(false)
public readonly initialized = ref(false)
public readonly currentFile = ref(null)
public readonly strudelBlocks = ref<Strudel[]>([])
public static getInstance(): GlobalStore {
if (!GlobalStore.instance) {
GlobalStore.instance = new GlobalStore()
}
return GlobalStore.instance
}
public get app(): App {
return this._app
}
public getActiveEditor() {
const activeLeaf = GlobalStore.getInstance()
.app.workspace.getLeavesOfType('markdown')
.find(
(leaf: WorkspaceLeaf) =>
(leaf.view as MarkdownView).file?.path === this.currentBlock.value?.filePath
)
const view = activeLeaf?.view as MarkdownView
if (activeLeaf && view.editor) {
return (view.editor as any).cm as EditorView
}
}
public readonly currentBlock = ref<Strudel | null>(null)
public readonly isPlaying = ref(false)
public play(strudelBlock: Strudel) {
this.currentBlock.value = strudelBlock
this.repl.evaluate(strudelBlock.code)
this.isPlaying.value = true
}
public stop() {
this.repl.stop()
this.isPlaying.value = false
}
public drawer: any | null = null
// public resetMiniLocations() {
// const editor = this.getActiveEditor()
// if (this.currentBlock.value && editor) {
// updateMiniLocations(this.getActiveEditor(), [])
// }
// }
// public recalculateMiniLocations() {
// if (this.currentBlock.value) {
// const locations = recalculateMiniLocations(this.currentBlock.value.code)
// updateMiniLocations(this.getActiveEditor(), locations || [])
// }
// }
public async initStrudel() {
this.repl = await init({
afterEval: (options: any) => {
// console.log('afterEval', options.meta?.miniLocations)
if (options.meta?.miniLocations) {
const locations = options.meta.miniLocations
// for (const loc of locations) {
// loc[0] = loc[0] + this.currentBlock.lineFrom
// loc[1] = loc[1] + this.currentBlock.lineFrom
// }
const editor = this.getActiveEditor()
if (this.currentBlock.value && editor) {
this.currentBlock.value.playbackStarted()
updateMiniLocations(editor, locations || [], this.currentBlock.value.lineFrom)
}
}
const drawTime = [-2, 2]
this.drawer.setDrawTime(drawTime)
// invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler)
},
onToggle: (started: boolean) => {
console.log('onToggle', started)
if (started) {
this.currentBlock.value.playbackStarted()
this.drawer.start(this.repl.scheduler)
// if (this.solo) {
// // stop other repls when this one is started
// document.dispatchEvent(
// new CustomEvent('start-repl', {
// detail: this.id,
// })
// )
// }
} else {
this.drawer.stop()
const editor = this.getActiveEditor()
console.log('playback stopped')
if (editor) {
this.currentBlock.value?.playbackStopped()
console.log('clearing mini locations')
updateMiniLocations(editor, [], 0)
}
this.currentBlock.value = null
}
},
})
console.log(this.repl)
this.strudelInitialized.value = true
this.drawer = new Drawer(
(haps: any, time: any, _: any) => {
const editor = this.getActiveEditor()
if (!editor || !this.currentBlock.value) {
return
}
// console.log(this.currentBlock.value.lineFrom)
const currentFrame = haps.filter((hap: any) => hap.isActive(time))
highlightMiniLocations(editor, time, currentFrame)
const rootStyles = getComputedStyle(document.getElementsByTagName('body')[0])
const playheadColor = rootStyles.getPropertyValue('--text-accent').trim()
const inactiveColor = rootStyles
.getPropertyValue('--background-modifier-active-hover')
.trim()
const activeColor = rootStyles.getPropertyValue('--text-accent').trim()
const drawContext = this.currentBlock.value?.drawContext
if (this.currentBlock.value?.drawContext) {
drawPianoroll({
haps,
time,
ctx: drawContext,
drawTime: [-2, 2],
fold: 1,
// labels: true,
playheadColor,
inactive: inactiveColor,
active: activeColor,
fillActive: true,
strokeActive: false,
})
}
},
[-2, 2]
)
}
public init(app: App): void {
if (this.initialized.value) {
return
}
this._app = app
this.initialized.value = true
}
public destroy(): void {
if (!this.initialized.value) {
return
}
this.initialized.value = false
this.currentFile.value = null
this.strudelBlocks.value.splice(0, this.strudelBlocks.value.length)
}
}

View file

@ -1,3 +0,0 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.

View file

@ -1,460 +0,0 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
import { Scale } from '@tonaljs/tonal';
import { soundMap } from 'superdough';
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
};
const stripHtml = (html) => {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
const getDocLabel = (doc) => doc.name || doc.longname;
const buildParamsList = (params) =>
params?.length
? `
<div class="autocomplete-info-params-section">
<h4 class="autocomplete-info-section-title">Parameters</h4>
<ul class="autocomplete-info-params-list">
${params
.map(
({ name, type, description }) => `
<li class="autocomplete-info-param-item">
<span class="autocomplete-info-param-name">${name}</span>
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
</li>
`,
)
.join('')}
</ul>
</div>
`
: '';
const buildExamples = (examples) =>
examples?.length
? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
</div>
`[0];
const isValidDoc = (doc) => {
const label = getDocLabel(doc);
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
};
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() {
const soundDict = soundMap.get();
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
if (suffix && bank) banks.add(bank);
}
return Array.from(banks)
.sort()
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal
let scaleCompletions = [];
try {
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
} catch (e) {
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
}
// Valid mode values for voicing
const modeCompletions = [
{ label: 'below', type: 'mode' },
{ label: 'above', type: 'mode' },
{ label: 'duck', type: 'mode' },
{ label: 'root', type: 'mode' },
];
// Valid chord symbols from ireal dictionary plus empty string for major triads
const chordSymbols = ['', ...Object.keys(complex)].sort();
const chordSymbolCompletions = chordSymbols.map((symbol) => {
if (symbol === '') {
return {
label: 'major',
apply: '',
type: 'chord-symbol',
};
}
return {
label: symbol,
apply: symbol,
type: 'chord-symbol',
};
});
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
return {
...doc,
name: synonym,
longname: synonym,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
};
};
const jsdocCompletions = (() => {
const seen = new Set(); // avoid repetition
const completions = [];
for (const doc of jsdoc.docs) {
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
const docLabel = getDocLabel(doc);
// Remove duplicates
const synonyms = doc.synonyms || [];
let labels = [docLabel, ...synonyms];
for (const label of labels) {
// https://codemirror.net/docs/ref/#autocomplete.Completion
if (label && !seen.has(label)) {
seen.add(label);
completions.push({
label,
info: () => Autocomplete(getSynonymDoc(doc, label)),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
});
}
}
}
return completions;
})();
// --- Handler functions for each context ---
const pitchNames = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'E#',
'Fb',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'Bb',
'B',
'B#',
'Cb',
];
// Cached regex patterns for scaleHandler
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
function scaleHandler(context) {
// First check for scale context without quotes - block with empty completions
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
if (scaleNoQuotesContext) {
return {
from: scaleNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
if (scaleAfterColonContext) {
const text = scaleAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
const options = filteredScales.map((s) => ({
...s,
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
}));
const from = scaleAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
if (scalePreColonContext) {
if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) {
const text = scalePreColonContext.text;
const match = text.match(SCALE_PITCH_MATCH_REGEX);
const fragment = match ? match[0] : '';
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const from = scalePreColonContext.to - fragment.length;
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options };
} else {
return { from: scalePreColonContext.to, options: [] };
}
}
}
return null;
}
// Cached regex patterns for soundHandler
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
function soundHandler(context) {
// First check for sound context without quotes - block with empty completions
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
if (soundNoQuotesContext) {
return {
from: soundNoQuotesContext.to,
options: [],
};
}
// Then check for sound context with quotes - provide completions
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
if (!soundContext) return null;
const text = soundContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap.get()).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
return {
from,
options,
};
}
// Cached regex patterns for bankHandler
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
function bankHandler(context) {
// First check for bank context without quotes - block with empty completions
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
if (bankNoQuotesContext) {
return {
from: bankNoQuotesContext.to,
options: [],
};
}
// Then check for bank context with quotes - provide completions
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
if (!bankMatch) return null;
const text = bankMatch.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragment = inside;
let banks = bankCompletions();
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
const from = bankMatch.to - fragment.length;
return {
from,
options: filteredBanks,
};
}
// Cached regex patterns for modeHandler
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
function modeHandler(context) {
// First check for mode context without quotes - block with empty completions
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
if (modeNoQuotesContext) {
return {
from: modeNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
if (modeAfterColonContext) {
const text = modeAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
// For anchor after colon, we can suggest pitch names
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
const from = modeAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
if (!modeContext) return null;
const text = modeContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
const from = modeContext.to - fragment.length;
return {
from,
options: filteredModes,
};
}
// Cached regex patterns for chordHandler
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
function chordHandler(context) {
// First check for chord context without quotes - block with empty completions
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
if (chordNoQuotesContext) {
return {
from: chordNoQuotesContext.to,
options: [],
};
}
// Then check for chord context with quotes - provide completions
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
if (!chordContext) return null;
const text = chordContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
// Use same fragment matching as sound/mode for expressions like "<G Am>"
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
// Check if fragment contains any pitch name at start (for root + symbol)
let rootMatch = null;
let symbolFragment = fragment;
for (const pitch of pitchNames) {
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
rootMatch = pitch;
symbolFragment = fragment.slice(pitch.length);
break;
}
}
if (rootMatch) {
// We have a root, now complete chord symbols
const filteredSymbols = chordSymbolCompletions.filter((s) =>
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
);
// Create completions that replace the entire chord, not just the symbol part
const options = filteredSymbols;
const from = chordContext.to - symbolFragment.length;
return { from, options };
} else {
// No root yet, complete with pitch names
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
const from = chordContext.to - fragment.length;
return { from, options };
}
}
// Cached regex patterns for fallbackHandler
const FALLBACK_WORD_REGEX = /\w*/;
function fallbackHandler(context) {
const word = context.matchBefore(FALLBACK_WORD_REGEX);
if (word && word.from === word.to && !context.explicit) return null;
if (word) {
return {
from: word.from,
options: jsdocCompletions,
};
}
return null;
}
const handlers = [
soundHandler,
bankHandler,
chordHandler,
scaleHandler,
modeHandler,
// this handler *must* be last
fallbackHandler,
];
export const strudelAutocomplete = (context) => {
for (const handler of handlers) {
const result = handler(context);
if (result) {
return result;
}
}
return null;
};
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];

View file

@ -1,63 +0,0 @@
import {
keymap,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
rectangularSelection,
crosshairCursor,
lineNumbers,
highlightActiveLineGutter,
} from '@codemirror/view';
import {
defaultHighlightStyle,
syntaxHighlighting,
bracketMatching,
foldGutter,
foldKeymap,
} from '@codemirror/language';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
export const basicSetup = (() => [
// lineNumbers(),
// highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
// foldGutter(),
// drawSelection(),
dropCursor(),
// EditorState.allowMultipleSelections.of(true),
// indentOnInput(),
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
// autocompletion(),
rectangularSelection(),
crosshairCursor(),
// highlightActiveLine(),
// highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
// ...searchKeymap,
...historyKeymap,
// ...foldKeymap,
// ...completionKeymap,
]),
])();
/// A minimal set of extensions to create a functional editor. Only
/// includes [the default keymap](#commands.defaultKeymap), [undo
/// history](#commands.history), [special character
/// highlighting](#view.highlightSpecialChars), [custom selection
/// drawing](#view.drawSelection), and [default highlight
/// style](#language.defaultHighlightStyle).
export const minimalSetup = (() => [
highlightSpecialChars(),
history(),
drawSelection(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([...defaultKeymap, ...historyKeymap]),
])();

View file

@ -1,386 +0,0 @@
import { closeBrackets } from '@codemirror/autocomplete';
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { indentWithTab } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
EditorView,
highlightActiveLineGutter,
highlightActiveLine,
keymap,
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { repl, registerControl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { basicSetup } from './basicSetup.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
isTooltipEnabled,
isPatternHighlightingEnabled,
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
keybindings,
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
isMultiCursorEnabled: (on) =>
on
? [
EditorState.allowMultipleSelections.of(true),
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
]
: [],
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
export const defaultSettings = {
keybindings: 'codemirror',
isBracketMatchingEnabled: false,
isBracketClosingEnabled: true,
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
isPatternHighlightingEnabled: true,
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
};
export const codemirrorSettings = persistentAtom('codemirror-settings', defaultSettings, {
encode: JSON.stringify,
decode: JSON.parse,
});
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
initTheme(settings.theme);
let state = EditorState.create({
doc: initialCode,
extensions: [
/* search(),
highlightSelectionMatches(), */
...initialSettings,
basicSetup,
mondo ? [] : javascript(),
javascriptLanguage.data.of({
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
}),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
Prec.highest(
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
},
{
key: 'Alt-.',
preventDefault: true,
run: () => onStop?.(),
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
),
],
});
return new EditorView({
state,
parent: root,
});
}
export class StrudelMirror {
constructor(options) {
const {
root,
id,
initialCode = '',
onDraw,
drawContext,
drawTime = [0, 0],
autodraw,
prebake,
bgFill = true,
solo = true,
...replOptions
} = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.drawTime = drawTime;
this.drawContext = drawContext;
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
this.highlight(currentFrame, time);
this.onDraw(haps, time, painters);
}, drawTime);
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
this.repl = repl({
...replOptions,
id,
onToggle: (started) => {
replOptions?.onToggle?.(started);
if (started) {
this.drawer.start(this.repl.scheduler);
if (this.solo) {
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
}
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(true, id);
}
},
beforeEval: async () => {
cleanupDraw(true, id);
await this.prebaked;
await replOptions?.beforeEval?.();
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
const sliders = this.widgets.filter((w) => w.type === 'slider');
updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler);
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
if (v.docChanged) {
this.code = v.state.doc.toString();
this.repl.setCode?.(this.code);
}
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
mondo: replOptions.mondo,
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.display = 'block';
if (bgFill) {
this.root.style.backgroundColor = 'var(--background)';
}
cmEditor.style.backgroundColor = 'transparent';
}
const settings = codemirrorSettings.get();
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (this.solo && e.detail !== this.id) {
this.stop();
}
};
document.addEventListener('start-repl', this.onStartRepl);
}
draw(haps, time, painters) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
}
// draw first frame instantly
await this.prebaked;
try {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.drawer.painters);
} catch (err) {
console.warn('first frame could not be painted');
}
}
async evaluate() {
this.flash();
await this.repl.evaluate(this.code);
}
async stop() {
this.repl.scheduler.stop();
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
} else {
this.evaluate();
}
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
const scroller = this.root.querySelector('.cm-scroller');
if (scroller) {
scroller.style.fontFamily = family;
}
}
reconfigureExtension(key, value) {
if (!extensions[key]) {
console.warn(`extension ${key} is not known`);
return;
}
value = parseBooleans(value);
const newValue = extensions[key](value, this);
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
if (key === 'theme') {
activateTheme(value);
}
}
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setBracketClosingEnabled(enabled) {
this.reconfigureExtension('isBracketClosingEnabled', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
setAutocompletionEnabled(enabled) {
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
}
updateSettings(settings) {
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
const updated = { ...codemirrorSettings.get(), ...settings };
codemirrorSettings.set(updated);
}
changeSetting(key, value) {
if (extensions[key]) {
this.reconfigureExtension(key, value);
return;
} else if (key === 'fontFamily') {
this.setFontFamily(value);
} else if (key === 'fontSize') {
this.setFontSize(value);
}
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
}
getCursorLocation() {
return this.editor.state.selection.main.head;
}
setCursorLocation(col) {
return this.editor.dispatch({ selection: { anchor: col } });
}
appendCode(code) {
const cursor = this.getCursorLocation();
this.setCode(this.code + code);
this.setCursorLocation(cursor);
}
}
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}
// helper function to generate repl ids
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
/**
* Overrides the css of highlighted events. Make sure to use single quotes!
* @name markcss
* @example
* note("c a f e")
* .markcss('text-decoration:underline')
*/
export const markcss = registerControl('markcss');

View file

@ -1,39 +0,0 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setFlash = StateEffect.define();
export const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
});
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export const isFlashEnabled = (on) => (on ? flashField : []);

View file

@ -1,138 +0,0 @@
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations) => {
view.dispatch({ effects: setMiniLocations.of(locations) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
};
const miniLocations = StateField.define({
create() {
return Decoration.none;
},
update(locations, tr) {
if (tr.docChanged) {
locations = locations.map(tr.changes);
}
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
// this is called on eval, with the mini locations obtained from the transpiler
// codemirror will automatically remap the marks when the document is edited
// create a mark for each mini location, adding the range to the spec to find it later
const marks = e.value
.filter(([from]) => from < tr.newDoc.length)
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
.map(
(range) =>
Decoration.mark({
id: range.join(':'),
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
attributes: { style: `background-color: #00CA2880` },
}).range(...range), // -> Decoration
);
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
}
}
return locations;
},
});
const visibleMiniLocations = StateField.define({
create() {
return { atTime: 0, haps: new Map() };
},
update(visible, tr) {
for (let e of tr.effects) {
if (e.is(showMiniLocations)) {
// this is called every frame to show the locations that are currently active
// we can NOT create new marks because the context.locations haven't changed since eval time
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
if (!hap.context?.locations || !hap.whole) {
continue;
}
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
haps.set(id, hap);
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
return visible;
},
});
// // Derive the set of decorations from the miniLocations and visibleLocations
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
const iterator = state.field(miniLocations).iter();
const { haps } = state.field(visibleMiniLocations);
const builder = new RangeSetBuilder();
while (iterator.value) {
const {
from,
to,
value: {
spec: { id },
},
} = iterator;
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)';
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
let channels = getComputedStyle(swatch)
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
.slice(1)
.map((c) => parseFloat(c || 1));
document.body.removeChild(swatch);
// Get percentage of event
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
channels[3] *= percent;
*/
builder.add(
from,
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style },
}),
);
}
iterator.next();
}
return builder.finish();
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
export const isPatternHighlightingEnabled = (on, config) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? Prec.highest(highlightExtension) : [];
};

View file

@ -1,18 +0,0 @@
export let html = (string) => {
const template = document.createElement('template');
template.innerHTML = string.trim();
return template.content.childNodes;
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
if (chunk === undefined) return '';
return chunk;
};
export let h = (strings, ...vars) => {
let string = '';
for (let i in strings) {
string += parseChunk(strings[i]);
string += parseChunk(vars[i]);
}
return html(string);
};

View file

@ -1,6 +0,0 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';
export * from './widget.mjs';

View file

@ -1,32 +0,0 @@
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor() {}
},
{
provide: () => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [active ? Prec.high(active()) : []];
}

View file

@ -1,58 +0,0 @@
{
"name": "@strudel/codemirror",
"version": "1.2.5",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.18.4",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.8",
"@codemirror/search": "^6.5.8",
"@codemirror/state": "^6.5.1",
"@codemirror/view": "^6.36.2",
"@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
"superdough": "workspace:*",
"nanostores": "^0.11.3"
},
"devDependencies": {
"vite": "^6.0.11"
}
}

View file

@ -1,146 +0,0 @@
import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
toDOM() {
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
wrap.className = 'cm-slider'; // inline-flex items-center
let slider = wrap.appendChild(document.createElement('input'));
slider.type = 'range';
slider.min = this.min;
slider.max = this.max;
slider.step = this.step ?? (this.max - this.min) / 1000;
slider.originalValue = this.value;
// to make sure the code stays in sync, let's save the original value
// becuase .value automatically clamps values so it'll desync with the code
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
const next = e.target.value;
let insert = next;
//let insert = next.toFixed(2);
const to = slider.from + slider.originalValue.length;
let change = { from: slider.from, to, insert };
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
}
ignoreEvent(e) {
return true;
}
}
export const setSliderWidgets = StateEffect.define();
export const updateSliderWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) });
};
function getSliders(widgetConfigs, view) {
return widgetConfigs
.filter((w) => w.type === 'slider')
.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
while (iterator.value) {
// when the widgets are moved, we need to tell the dom node the current position
// this is important because the updateSliderValue function has to work with the dom node
if (iterator.value?.widget?.slider) {
iterator.value.widget.slider.from = iterator.from;
iterator.value.widget.slider.to = iterator.to;
}
iterator.next();
}
}
for (let e of tr.effects) {
if (e.is(setSliderWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
/**
* Displays a slider widget to allow the user manipulate a value
*
* @name slider
* @param {number} value Initial value
* @param {number} min Minimum value - optional, defaults to 0
* @param {number} max Maximum value - optional, defaults to 1
* @param {number} step Step size - optional
*/
export let slider = (value) => {
console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value);
};
// function transpiled from slider = (value, min, max)
export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
if (sliderValues[e.data.id] !== undefined) {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
}

View file

@ -1,218 +0,0 @@
import strudelTheme, { settings as strudelThemeSettings } from './themes/strudel-theme.mjs';
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen.mjs';
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen.mjs';
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
import aura, { settings as auraSettings } from './themes/aura.mjs';
import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
import gruvboxDark, { settings as gruvboxDarkSettings } from './themes/gruvboxDark.mjs';
import gruvboxLight, { settings as gruvboxLightSettings } from './themes/gruvboxLight.mjs';
import materialDark, { settings as materialDarkSettings } from './themes/materialDark.mjs';
import materialLight, { settings as materialLightSettings } from './themes/materialLight.mjs';
import nord, { settings as nordSettings } from './themes/nord.mjs';
import monokai, { settings as monokaiSettings } from './themes/monokai.mjs';
import solarizedDark, { settings as solarizedDarkSettings } from './themes/solarizedDark.mjs';
import solarizedLight, { settings as solarizedLightSettings } from './themes/solarizedLight.mjs';
import sublime, { settings as sublimeSettings } from './themes/sublime.mjs';
import tokyoNight, { settings as tokyoNightSettings } from './themes/tokyoNight.mjs';
import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/tokioNightStorm.mjs';
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
algoboy,
archBtw,
androidstudio,
atomone,
aura,
bbedit,
blackscreen,
bluescreen,
bluescreenlight,
CutiePi,
darcula,
dracula,
duotoneDark,
eclipse,
fruitDaw,
githubDark,
githubLight,
greenText,
gruvboxDark,
gruvboxLight,
sonicPink,
materialDark,
materialLight,
monokai,
noctisLilac,
nord,
redText,
solarizedDark,
solarizedLight,
sublime,
teletext,
tokyoNight,
tokyoNightDay,
tokyoNightStorm,
vscodeDark,
vscodeLight,
whitescreen,
xcodeLight,
};
export const settings = {
strudelTheme: strudelThemeSettings,
bluescreen: bluescreenSettings,
bluescreenlight: bluescreenlightsettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
archBtw: archBtwSettings,
androidstudio: androidstudioSettings,
atomone: atomOneSettings,
aura: auraSettings,
bbedit: bbeditSettings,
darcula: darculaSettings,
dracula: draculaSettings,
duotoneDark: duotoneDarkSettings,
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
fruitDaw: fruitDawSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings,
materialLight: materialLightSettings,
noctisLilac: noctisLilacSettings,
nord: nordSettings,
monokai: monokaiSettings,
redText: redTextSettings,
solarizedLight: solarizedLightSettings,
solarizedDark: solarizedDarkSettings,
sublime: sublimeSettings,
tokyoNight: tokyoNightSettings,
tokyoNightStorm: tokyoNightStormSettings,
vscodeDark: vscodeDarkSettings,
vscodeLight: vscodeLightSettings,
xcodeLight: xcodeLightSettings,
tokyoNightDay: tokyoNightDaySettings,
};
function getColors(str) {
const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
const colors = [];
let match;
while ((match = colorRegex.exec(str)) !== null) {
const color = match[0];
if (!colors.includes(color)) {
colors.push(color);
}
}
return colors;
}
// TODO: remove
export function themeColors(theme) {
return getColors(stringifySafe(theme));
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}
function stringifySafe(json) {
return JSON.stringify(json, getCircularReplacer());
}
export const theme = (theme) => themes[theme] || themes.strudelTheme;
// css style injection helpers
export function injectStyle(rule) {
const newStyle = document.createElement('style');
document.head.appendChild(newStyle);
const styleSheet = newStyle.sheet;
const ruleIndex = styleSheet.insertRule(rule, 0);
return () => styleSheet.deleteRule(ruleIndex);
}
let currentTheme,
resetThemeStyle,
themeStyle,
styleID = 'strudel-theme-vars';
export function initTheme(theme) {
if (!document.getElementById(styleID)) {
themeStyle = document.createElement('style');
themeStyle.id = styleID;
document.head.append(themeStyle);
}
activateTheme(theme);
}
export function activateTheme(name) {
if (currentTheme === name) {
return;
}
currentTheme = name;
if (!settings[name]) {
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
}
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')}
}`;
setTheme(themeSettings);
// tailwind dark mode
if (themeSettings.light) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
resetThemeStyle?.();
resetThemeStyle = undefined;
if (themeSettings.customStyle) {
resetThemeStyle = injectStyle(themeSettings.customStyle);
}
}

View file

@ -1,45 +0,0 @@
/*
* Cutie Pi
* by Switch Angel
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const deepPurple = '#5c019a';
const yellowPink = '#fbeffc';
const grey = '#272C35';
const pinkAccent = '#fee1ff';
const lightGrey = '#465063';
const bratGreen = '#9acd3f';
const lighterGrey = '#97a1b7';
const pink = '#f6a6fd';
export const settings = {
background: 'white',
lineBackground: 'transparent',
foreground: deepPurple,
caret: '#797977',
selection: yellowPink,
selectionMatch: '#2B323D',
gutterBackground: grey,
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: deepPurple,
},
{ tag: [t.tagName, t.heading], color: settings.foreground },
{ tag: t.comment, color: lighterGrey },
{ tag: [t.variableName, t.propertyName, t.labelName], color: pink },
{ tag: [t.attributeName, t.number], color: '#d19a66' },
{ tag: t.className, color: grey },
{ tag: t.keyword, color: deepPurple },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen },
],
});

View file

@ -1,61 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const palettes = {
// https://www.deviantart.com/advancedfan2020/art/Game-Boy-Palette-Set-Color-HEX-Part-09-920495662
'Central Florida A': ['#FFF630', '#B3AC22', '#666213', '#191905'],
'Central Florida B': ['#38CEBA', '#279082', '#16524A', '#061513'],
'Central Florida C': ['#FF8836', '#B35F26', '#663616', '#190E05'],
'Central Florida D': ['#E07070', '#9D4E4E', '#5A2D2D', '#160B0B'],
'Central Florida E': ['#7AA4CB', '#55738E', '#314251', '#0C1014'],
'Feminine Energy A': ['#DC5686', '#9A415E', '#582536', '#16090D'],
'Feminine Energy B': ['#D0463C', '#92312A', '#531c18', '#150706'],
'Feminine Energy C': ['#D86918', '#974A11', '#562A0A', '#160A02'],
'Feminine Energy D': ['#EFC54F', '#A78A36', '#604F20', '#181408'],
'Feminine Energy E': ['#866399', '#5e456b', '#36283d', '#0d0a0f'],
'Sour Watermelon A': ['#993366', '#6B2447', '#3D1429', '#0F050A'],
'Sour Watermelon B': ['#996666', '#6B4747', '#3D2929', '#0F0A0A'],
'Sour Watermelon C': ['#999966', '#686B47', '#3d3d29', '#0f0f0A'],
'Sour Watermelon D': ['#99cc66', '#6b8f47', '#3d5229', '#0f140a'],
'Sour Watermelon E': ['#99ff66', '#6bb347', '#3d6629', '#0f190a'],
//https://www.deviantart.com/advancedfan2020/art/Game-Boy-Palette-Set-Color-HEX-Part-02-920073260
'Peri Peaceful A': ['#909BE9', '#656DA3', '#3A3E5D', '#0e0f17'],
'Peri Peaceful B': ['#68628d', '#494563', '#2a2738', '#0a0a0e'], // pretty dim
'Peri Peaceful E': ['#b5a0a9', '#7f7076', '#484044', '#121011'],
'Hichem Palette B': ['#4fa3a5', '#377273', '#204142', '#081010'],
'Hichem Palette C': ['#Fe6f9b', '#b24e6d', '#662c3e', '#190b0f'],
'Hichem Palette D': ['#ffbb5a', '#b3833f', '#664b24', '#191309'],
'JSR2 A': ['#E0EFC0', '#9da786', '#5a604d', '#161813'],
};
const palette = palettes['Sour Watermelon B'];
export const settings = {
background: palette[3],
foreground: palette[1],
caret: palette[0],
selection: palette[0],
selectionMatch: palette[1],
lineHighlight: palette[3],
lineBackground: palette[3] + '90',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: palette[0],
light: false,
// customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.comment, color: palette[2] },
{ tag: t.string, color: palette[1] },
{ tag: [t.atom, t.number], color: palette[1] },
{ tag: [t.meta, t.labelName, t.variableName], color: palette[0] },
{
tag: [t.keyword, t.tagName, t.arithmeticOperator],
color: palette[1],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] },
{ tag: [t.function(t.variableName), t.propertyName], color: palette[0] },
{ tag: t.atom, color: palette[1] },
],
});

View file

@ -1,43 +0,0 @@
/*
* androidstudio
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
lineHighlight: '#343739',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#282b2e',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#4e5254',
selectionMatch: '#4e5254',
gutterForeground: '#cccccc50',
lineHighlight: '#7f85891f',
},
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: [t.keyword, t.deleted, t.className], color: '#a9b7c6' },
{ tag: [t.number, t.literal], color: '#6897bb' },
//{ tag: [t.link, t.variableName], color: '#629755' },
{ tag: [t.link, t.variableName], color: '#a9b7c6' },
{ tag: [t.comment, t.quote], color: 'grey' },
{ tag: [t.meta, t.documentMeta], color: '#bbb529' },
//{ tag: [t.string, t.propertyName, t.attributeValue], color: '#6a8759' },
{ tag: [t.propertyName, t.attributeValue], color: '#a9b7c6' },
{ tag: [t.string], color: '#6a8759' },
{ tag: [t.heading, t.typeName], color: '#ffc66d' },
{ tag: [t.attributeName], color: '#a9b7c6' },
{ tag: [t.emphasis], fontStyle: 'italic' },
],
});

View file

@ -1,38 +0,0 @@
/*
* Arch Btw
* Modern terminal inspired theme
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[1],
selection: hex[2],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[1],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[1] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

View file

@ -1,51 +0,0 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#272C35',
lineBackground: '#272C3599',
foreground: 'hsl(220, 14%, 71%)',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
gutterBackground: '#272C35',
gutterForeground: '#465063',
gutterBorder: 'transparent',
lineHighlight: '#2B323D',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#272C35',
foreground: '#9d9b97',
caret: '#797977',
selection: '#3d4c64',
selectionMatch: '#3d4c64',
gutterBackground: '#272C35',
gutterForeground: '#465063',
gutterBorder: 'transparent',
lineHighlight: '#2e3f5940',
},
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: 'hsl(207, 82%, 66%)',
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' },
{ tag: [t.tagName, t.heading], color: '#e06c75' },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
{ tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
{ tag: t.className, color: 'hsl( 39, 67%, 69%)' },
{ tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
],
});

View file

@ -1,51 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
gutterBackground: '#21202e',
gutterForeground: '#edecee',
gutterBorder: 'transparent',
lineHighlight: '#a394f033',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#21202e',
foreground: '#edecee',
caret: '#a277ff',
selection: '#5a51898f',
selectionMatch: '#5a51898f',
gutterBackground: '#21202e',
gutterForeground: '#edecee',
gutterBorder: 'transparent',
lineHighlight: '#a394f033',
},
styles: [
{ tag: t.keyword, color: '#a277ff' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#edecee' },
{ tag: [t.propertyName], color: '#ffca85' },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#61ffca' },
{ tag: [t.function(t.variableName), t.labelName], color: '#ffca85' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#61ffca' },
{ tag: [t.definition(t.name), t.separator], color: '#edecee' },
{ tag: [t.className], color: '#82e2ff' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#61ffca' },
{ tag: [t.typeName], color: '#82e2ff' },
{ tag: [t.operator, t.operatorKeyword], color: '#a277ff' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#61ffca' },
{ tag: [t.meta, t.comment], color: '#6d6d6d' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#a277ff' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#edecee' },
{ tag: t.invalid, color: '#ff6767' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,46 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
gutterBackground: '#f5f5f5',
gutterForeground: '#4D4D4C',
gutterBorder: 'transparent',
lineHighlight: '#00000012',
};
export default createTheme({
theme: 'light',
settings: {
background: '#FFFFFF',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
gutterBackground: '#f5f5f5',
gutterForeground: '#4D4D4C',
gutterBorder: 'transparent',
lineHighlight: '#00000012',
},
styles: [
{ tag: [t.meta, t.comment], color: '#804000' },
{ tag: [t.keyword, t.strong], color: '#0000FF' },
{ tag: [t.number], color: '#FF0080' },
{ tag: [t.string], color: '#FF0080' },
{ tag: [t.variableName], color: '#006600' },
{ tag: [t.escape], color: '#33CC33' },
{ tag: [t.tagName], color: '#1C02FF' },
{ tag: [t.heading], color: '#0C07FF' },
{ tag: [t.quote], color: '#000000' },
{ tag: [t.list], color: '#B90690' },
{ tag: [t.documentMeta], color: '#888888' },
{ tag: [t.function(t.variableName)], color: '#0000A2' },
{ tag: [t.definition(t.typeName), t.typeName], color: '#6D79DE' },
],
});

View file

@ -1,39 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
],
});

View file

@ -1,42 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
],
});

View file

@ -1,37 +0,0 @@
/*
* A lighter blue screen theme
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[3],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[1],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
{ tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[2] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
],
});

View file

@ -1,47 +0,0 @@
/*
* darcula
* Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#242424',
lineBackground: '#24242499',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: 'rgba(255, 255, 255, 0.1)',
gutterForeground: '#999',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#242424',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: 'transparent',
gutterForeground: '#999',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
styles: [
{ tag: t.labelName, color: '#CCCCCC' },
{ tag: [t.atom, t.number], color: '#7A9EC2' },
{ tag: [t.comment], color: '#707070' },
{ tag: [t.string], color: '#6A8759' },
{ tag: [t.variableName, t.operator], color: '#CCCCCC' },
{ tag: [t.function(t.variableName), t.propertyName], color: '#FFC66D' },
{ tag: [t.meta, t.className], color: '#FFC66D' },
{ tag: [t.propertyName], color: '#FFC66D' },
{ tag: [t.keyword], color: '#CC7832' },
{ tag: [t.tagName], color: '#ff79c6' },
{ tag: [t.typeName], color: '#ffb86c' },
],
});

View file

@ -1,50 +0,0 @@
/*
* @name dracula
* Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/
// this is different from https://thememirror.net/dracula
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: '#282a36',
gutterForeground: '#6272a4',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
};
const purple = '#BD93F9';
export default createTheme({
theme: 'dark',
settings: {
background: '#282a36',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: '#282a36',
gutterForeground: '#6272a4',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
styles: [
{ tag: t.comment, color: '#6272a4' },
{ tag: t.string, color: '#f1fa8c' },
{ tag: [t.atom, t.number], color: purple },
{ tag: [t.meta, t.labelName, t.variableName], color: '#f8f8f2' },
{
tag: [t.keyword, t.tagName, t.arithmeticOperator],
color: '#ff79c6',
},
{ tag: [t.function(t.variableName), t.propertyName], color: '#50fa7b' },
{ tag: t.atom, color: '#bd93f9' },
],
});

View file

@ -1,42 +0,0 @@
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#eeebff',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
gutterForeground: '#545167',
lineHighlight: '#36334280',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#2a2734',
foreground: '#6c6783',
caret: '#ffad5c',
selection: '#9a86fd',
selectionMatch: '#9a86fd',
gutterBackground: '#2a2734',
gutterForeground: '#545167',
lineHighlight: '#36334280',
},
styles: [
{ tag: [t.comment, t.bracket, t.operator], color: '#6c6783' },
{ tag: [t.atom, t.number, t.keyword, t.link, t.attributeName, t.quote], color: '#ffcc99' },
{ tag: [t.emphasis, t.heading, t.tagName, t.propertyName, t.className, t.variableName], color: '#eeebff' },
{ tag: [t.typeName, t.url], color: '#eeebff' },
{ tag: t.string, color: '#ffb870' },
/* { tag: [t.propertyName], color: '#9a86fd' }, */
{ tag: [t.propertyName], color: '#eeebff' },
{ tag: t.labelName, color: '#eeebff' },
],
});

View file

@ -1,45 +0,0 @@
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#faf8f5',
lineBackground: '#faf8f599',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
selectionMatch: '#e3dcce',
gutterBackground: '#faf8f5',
gutterForeground: '#cdc4b1',
gutterBorder: 'transparent',
lineHighlight: '#EFEFEF',
};
export default createTheme({
theme: 'light',
settings: {
background: '#faf8f5',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
selectionMatch: '#e3dcce',
gutterBackground: '#faf8f5',
gutterForeground: '#cdc4b1',
gutterBorder: 'transparent',
lineHighlight: '#ddceb154',
},
styles: [
{ tag: [t.comment, t.bracket], color: '#b6ad9a' },
{ tag: [t.atom, t.number, t.keyword, t.link, t.attributeName, t.quote], color: '#063289' },
{ tag: [t.emphasis, t.heading, t.tagName, t.propertyName, t.variableName], color: '#2d2006' },
{ tag: [t.typeName, t.url, t.string], color: '#896724' },
{ tag: [t.operator, t.string], color: '#1659df' },
{ tag: [t.propertyName], color: '#b29762' },
{ tag: [t.unit, t.punctuation], color: '#063289' },
],
});

View file

@ -1,46 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#f7f7f7',
gutterForeground: '#999',
lineHighlight: '#e8f2ff',
gutterBorder: 'transparent',
};
export default createTheme({
theme: 'light',
settings: {
background: '#fff',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#f7f7f7',
gutterForeground: '#999',
lineHighlight: '#006fff1c',
gutterBorder: 'transparent',
},
styles: [
{ tag: [t.comment], color: '#3F7F5F' },
{ tag: [t.documentMeta], color: '#FF1717' },
{ tag: t.keyword, color: '#7F0055', fontWeight: 'bold' },
{ tag: t.atom, color: '#00f' },
{ tag: t.number, color: '#164' },
{ tag: t.propertyName, color: '#164' },
{ tag: [t.variableName, t.definition(t.variableName)], color: '#0000C0' },
{ tag: t.function(t.variableName), color: '#0000C0' },
{ tag: t.string, color: '#2A00FF' },
{ tag: t.operator, color: 'black' },
{ tag: t.tagName, color: '#170' },
{ tag: t.attributeName, color: '#00c' },
{ tag: t.link, color: '#219' },
],
});

View file

@ -1,50 +0,0 @@
/*
* Fruit Daw
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = [
'rgb(84, 93, 98)',
'rgb(255, 255, 255)',
'rgba(255, 255, 255, .25)',
'rgb(67, 76, 81)',
'rgb(186, 230, 115)',
'rgb(252, 184, 67)',
'rgb(124, 206, 254)',
'rgb(83, 101, 102)',
'rgba(46, 62, 72,.5)',
'rgb(94, 100, 108)',
'rgb(167, 216, 177)',
];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[10],
selection: hex[8],
selectionMatch: hex[0],
gutterBackground: hex[3],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[3],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[1],
},
{ tag: [t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
{ tag: [t.variableName], color: hex[1] },
{ tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
{ tag: [t.attributeName, t.number], color: hex[6] },
{ tag: t.keyword, color: hex[5] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
],
});

View file

@ -1,42 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
lineHighlight: '#36334280',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#0d1117',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
lineHighlight: '#36334280',
},
styles: [
{ tag: t.labelName, color: '#d2a8ff' },
{ tag: [t.standard(t.tagName), t.tagName], color: '#7ee787' },
{ tag: [t.comment, t.bracket], color: '#8b949e' },
{ tag: [t.className, t.propertyName], color: '#d2a8ff' },
{ tag: [t.variableName, t.attributeName], color: '#d2a8ff' },
{ tag: [t.number, t.operator], color: '#79c0ff' },
{ tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#ff7b72' },
{ tag: [t.string, t.meta, t.regexp], color: '#a5d6ff' },
{ tag: [t.name, t.quote], color: '#7ee787' },
{ tag: [t.heading, t.strong], color: '#d2a8ff', fontWeight: 'bold' },
{ tag: [t.emphasis], color: '#d2a8ff', fontStyle: 'italic' },
{ tag: [t.deleted], color: '#ffdcd7', backgroundColor: 'ffeef0' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#ffab70' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.invalid, color: '#f97583' },
],
});

View file

@ -1,42 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#6e7781',
};
export default createTheme({
theme: 'light',
settings: {
background: '#fff',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#6e7781',
},
styles: [
{ tag: [t.standard(t.tagName), t.tagName], color: '#116329' },
{ tag: [t.comment, t.bracket], color: '#6a737d' },
{ tag: [t.className, t.propertyName], color: '#6f42c1' },
{ tag: [t.variableName, t.attributeName, t.number, t.operator], color: '#005cc5' },
{ tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#d73a49' },
{ tag: [t.string, t.meta, t.regexp], color: '#032f62' },
{ tag: [t.name, t.quote], color: '#22863a' },
{ tag: [t.heading, t.strong], color: '#24292e', fontWeight: 'bold' },
{ tag: [t.emphasis], color: '#24292e', fontStyle: 'italic' },
{ tag: [t.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#e36209' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#032f62' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.invalid, color: '#cb2431' },
],
});

View file

@ -1,39 +0,0 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

View file

@ -1,82 +0,0 @@
/*
* gruvbox-dark
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
lineHighlight: '#3c3836',
gutterBackground: '#282828',
gutterForeground: '#7c6f64',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#282828',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#b99d555c',
selectionMatch: '#b99d555c',
lineHighlight: '#baa1602b',
gutterBackground: '#282828',
gutterForeground: '#7c6f64',
},
styles: [
{ tag: t.keyword, color: '#fb4934' },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: '#8ec07c' },
{ tag: [t.variableName], color: '#83a598' },
{ tag: [t.function(t.variableName)], color: '#8ec07c', fontStyle: 'bold' },
{ tag: [t.labelName], color: '#ebdbb2' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#d3869b' },
{ tag: [t.definition(t.name), t.separator], color: '#ebdbb2' },
{ tag: [t.brace], color: '#ebdbb2' },
{ tag: [t.annotation], color: '#fb4934d' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#d3869b' },
{ tag: [t.typeName, t.className], color: '#fabd2f' },
{ tag: [t.operatorKeyword], color: '#fb4934' },
{
tag: [t.tagName],
color: '#8ec07c',
fontStyle: 'bold',
},
{ tag: [t.squareBracket], color: '#fe8019' },
{ tag: [t.angleBracket], color: '#83a598' },
{ tag: [t.attributeName], color: '#8ec07c' },
{ tag: [t.regexp], color: '#8ec07c' },
{ tag: [t.quote], color: '#928374' },
{ tag: [t.string], color: '#ebdbb2' },
{
tag: t.link,
color: '#a89984',
textDecoration: 'underline',
textUnderlinePosition: 'under',
},
{ tag: [t.url, t.escape, t.special(t.string)], color: '#d3869b' },
{ tag: [t.meta], color: '#fabd2f' },
{ tag: [t.comment], color: '#928374', fontStyle: 'italic' },
{ tag: t.strong, fontWeight: 'bold', color: '#fe8019' },
{ tag: t.emphasis, fontStyle: 'italic', color: '#b8bb26' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.heading, fontWeight: 'bold', color: '#b8bb26' },
{ tag: [t.heading1, t.heading2], fontWeight: 'bold', color: '#b8bb26' },
{
tag: [t.heading3, t.heading4],
fontWeight: 'bold',
color: '#fabd2f',
},
{ tag: [t.heading5, t.heading6], color: '#fabd2f' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#d3869b' },
{ tag: [t.processingInstruction, t.inserted], color: '#83a598' },
{ tag: [t.contentSeparator], color: '#fb4934' },
{ tag: t.invalid, color: '#fe8019', borderBottom: `1px dotted #fb4934d` },
],
});

View file

@ -1,130 +0,0 @@
/*
* gruvbox-light
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
lineHighlight: '#ebdbb2',
gutterBackground: '#ebdbb2',
gutterForeground: '#665c54',
gutterBorder: 'transparent',
};
export default createTheme({
theme: 'light',
settings: {
background: '#fbf1c7',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#bdae9391',
selectionMatch: '#bdae9391',
lineHighlight: '#a37f2238',
gutterBackground: '#ebdbb2',
gutterForeground: '#665c54',
gutterBorder: 'transparent',
},
styles: [
{ tag: t.keyword, color: '#9d0006' },
{
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
color: '#427b58',
},
{ tag: [t.variableName], color: '#076678' },
{ tag: [t.function(t.variableName)], color: '#79740e', fontStyle: 'bold' },
{ tag: [t.labelName], color: '#3c3836' },
{
tag: [t.color, t.constant(t.name), t.standard(t.name)],
color: '#8f3f71',
},
{ tag: [t.definition(t.name), t.separator], color: '#3c3836' },
{ tag: [t.brace], color: '#3c3836' },
{
tag: [t.annotation],
color: '#9d0006',
},
{
tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
color: '#8f3f71',
},
{
tag: [t.typeName, t.className],
color: '#b57614',
},
{
tag: [t.operator, t.operatorKeyword],
color: '#9d0006',
},
{
tag: [t.tagName],
color: '#427b58',
fontStyle: 'bold',
},
{
tag: [t.squareBracket],
color: '#af3a03',
},
{
tag: [t.angleBracket],
color: '#076678',
},
{
tag: [t.attributeName],
color: '#427b58',
},
{
tag: [t.regexp],
color: '#427b58',
},
{
tag: [t.quote],
color: '#928374',
},
{ tag: [t.string], color: '#3c3836' },
{
tag: t.link,
color: '#7c6f64',
textDecoration: 'underline',
textUnderlinePosition: 'under',
},
{
tag: [t.url, t.escape, t.special(t.string)],
color: '#8f3f71',
},
{ tag: [t.meta], color: '#b57614' },
{ tag: [t.comment], color: '#928374', fontStyle: 'italic' },
{ tag: t.strong, fontWeight: 'bold', color: '#af3a03' },
{ tag: t.emphasis, fontStyle: 'italic', color: '#79740e' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.heading, fontWeight: 'bold', color: '#79740e' },
{ tag: [t.heading1, t.heading2], fontWeight: 'bold', color: '#79740e' },
{
tag: [t.heading3, t.heading4],
fontWeight: 'bold',
color: '#b57614',
},
{
tag: [t.heading5, t.heading6],
color: '#b57614',
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#8f3f71' },
{
tag: [t.processingInstruction, t.inserted],
color: '#076678',
},
{
tag: [t.contentSeparator],
color: '#9d0006',
},
{ tag: t.invalid, color: '#af3a03', borderBottom: `1px dotted #9d0006` },
],
});

View file

@ -1,77 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#212121',
lineBackground: '#21212199',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#212121',
gutterForeground: '#999',
gutterActiveForeground: '#4f5b66',
lineHighlight: '#111111',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#212121',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f063',
selectionMatch: '#d7d4f063',
gutterBackground: '#212121',
gutterForeground: '#999',
gutterActiveForeground: '#4f5b66',
lineHighlight: '#333333',
},
styles: [
{ tag: t.keyword, color: '#cf6edf' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#56c8d8' },
{ tag: [t.propertyName], color: '#82AAFF' },
{ tag: [t.variableName], color: '#bdbdbd' },
{ tag: [t.function(t.variableName)], color: '#82AAFF' },
{ tag: [t.labelName], color: '#cf6edf' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#facf4e' },
{ tag: [t.definition(t.name), t.separator], color: '#56c8d8' },
{ tag: [t.brace], color: '#cf6edf' },
{ tag: [t.annotation], color: '#f07178' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#f07178' },
{ tag: [t.typeName, t.className], color: '#f07178' },
{ tag: [t.operator, t.operatorKeyword], color: '#82AAFF' },
{ tag: [t.tagName], color: '#99d066' },
{ tag: [t.squareBracket], color: '#f07178' },
{ tag: [t.angleBracket], color: '#606f7a' },
{ tag: [t.attributeName], color: '#bdbdbd' },
{ tag: [t.regexp], color: '#f07178' },
{ tag: [t.quote], color: '#6abf69' },
{ tag: [t.string], color: '#99d066' },
{
tag: t.link,
color: '#56c8d8',
textDecoration: 'underline',
textUnderlinePosition: 'under',
},
{ tag: [t.url, t.escape, t.special(t.string)], color: '#facf4e' },
{ tag: [t.meta], color: '#707d8b' },
{ tag: [t.comment], color: '#707d8b', fontStyle: 'italic' },
{ tag: t.monospace, color: '#bdbdbd' },
{ tag: t.strong, fontWeight: 'bold', color: '#f07178' },
{ tag: t.emphasis, fontStyle: 'italic', color: '#99d066' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.heading, fontWeight: 'bold', color: '#facf4e' },
{ tag: t.heading1, fontWeight: 'bold', color: '#facf4e' },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: 'bold',
color: '#facf4e',
},
{ tag: [t.heading5, t.heading6], color: '#facf4e' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#56c8d8' },
{ tag: [t.processingInstruction, t.inserted], color: '#f07178' },
{ tag: [t.contentSeparator], color: '#56c8d8' },
{ tag: t.invalid, color: '#606f7a', borderBottom: `1px dotted #f07178` },
],
});

View file

@ -1,52 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
gutterBackground: '#FAFAFA',
gutterForeground: '#90A4AE',
gutterBorder: 'transparent',
lineHighlight: '#CCD7DA50',
};
export default createTheme({
theme: 'light',
settings: {
background: '#FAFAFA',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#80CBC440',
gutterBackground: '#FAFAFA',
gutterForeground: '#90A4AE',
gutterBorder: 'transparent',
lineHighlight: '#CCD7DA50',
},
styles: [
{ tag: t.keyword, color: '#39ADB5' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#90A4AE' },
{ tag: [t.propertyName], color: '#6182B8' },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#91B859' },
{ tag: [t.function(t.variableName), t.labelName], color: '#6182B8' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#39ADB5' },
{ tag: [t.definition(t.name), t.separator], color: '#90A4AE' },
{ tag: [t.className], color: '#E2931D' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#F76D47' },
{ tag: [t.typeName], color: '#E2931D', fontStyle: '#E2931D' },
{ tag: [t.operator, t.operatorKeyword], color: '#39ADB5' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#91B859' },
{ tag: [t.meta, t.comment], color: '#90A4AE' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#39ADB5' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#90A4AE' },
{ tag: t.invalid, color: '#E5393570' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,45 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
gutterBackground: '#272822',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#272822',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
gutterBackground: '#272822',
gutterForeground: '#FFFFFF70',
lineHighlight: '#0000003b',
},
styles: [
{ tag: t.labelName, color: '#bababa' },
{ tag: [t.comment, t.documentMeta], color: '#8292a2' },
{ tag: [t.number, t.bool, t.null, t.atom], color: '#ae81ff' },
{ tag: [t.attributeValue, t.className, t.name], color: '#e6db74' },
{ tag: [t.propertyName, t.attributeName], color: '#a6e22e' },
{ tag: [t.variableName], color: '#9effff' },
{ tag: [t.squareBracket], color: '#bababa' },
{ tag: [t.string, t.special(t.brace)], color: '#e6db74' },
{ tag: [t.regexp, t.className, t.typeName, t.definition(t.typeName)], color: '#66d9ef' },
{
tag: [t.definition(t.variableName), t.definition(t.propertyName), t.function(t.variableName)],
color: '#a6e22e',
},
// { tag: t.keyword, color: '#f92672' },
{ tag: [t.keyword, t.definitionKeyword, t.modifier, t.tagName, t.angleBracket], color: '#f92672' },
],
});

View file

@ -1,50 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
gutterBackground: '#f2f1f8',
gutterForeground: '#0c006b70',
lineHighlight: '#e1def3',
};
export default createTheme({
theme: 'light',
settings: {
background: '#f2f1f8',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
gutterBackground: '#f2f1f8',
gutterForeground: '#0c006b70',
lineHighlight: '#16067911',
},
styles: [
{ tag: t.comment, color: '#9995b7' },
{
tag: t.keyword,
color: '#ff5792',
fontWeight: 'bold',
},
{ tag: [t.definitionKeyword, t.modifier], color: '#ff5792' },
{ tag: [t.className, t.tagName, t.definition(t.typeName)], color: '#0094f0' },
{ tag: [t.number, t.bool, t.null, t.special(t.brace)], color: '#5842ff' },
{ tag: [t.definition(t.propertyName), t.function(t.variableName)], color: '#0095a8' },
{ tag: t.typeName, color: '#b3694d' },
{ tag: [t.propertyName, t.variableName], color: '#fa8900' },
{ tag: t.operator, color: '#ff5792' },
{ tag: t.self, color: '#e64100' },
{ tag: [t.string, t.regexp], color: '#00b368' },
{ tag: [t.paren, t.bracket], color: '#0431fa' },
{ tag: t.labelName, color: '#00bdd6' },
{ tag: t.attributeName, color: '#e64100' },
{ tag: t.angleBracket, color: '#9995b7' },
],
});

View file

@ -1,78 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
gutterBackground: '#2e3440',
gutterForeground: '#4c566a',
gutterActiveForeground: '#d8dee9',
lineHighlight: '#4c566a',
};
// Colors from https://www.nordtheme.com/docs/colors-and-palettes
export default createTheme({
theme: 'dark',
settings: {
background: '#2e3440',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#00000073',
selectionMatch: '#00000073',
gutterBackground: '#2e3440',
gutterForeground: '#4c566a',
gutterActiveForeground: '#d8dee9',
lineHighlight: '#4c566a29',
},
styles: [
{ tag: t.keyword, color: '#5e81ac' },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: '#88c0d0' },
{ tag: [t.variableName], color: '#8fbcbb' },
{ tag: [t.function(t.variableName)], color: '#8fbcbb' },
{ tag: [t.labelName], color: '#81a1c1' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#5e81ac' },
{ tag: [t.definition(t.name), t.separator], color: '#a3be8c' },
{ tag: [t.brace], color: '#8fbcbb' },
{ tag: [t.annotation], color: '#d30102' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#b48ead' },
{ tag: [t.typeName, t.className], color: '#ebcb8b' },
{ tag: [t.operator, t.operatorKeyword], color: '#a3be8c' },
{ tag: [t.tagName], color: '#b48ead' },
{ tag: [t.squareBracket], color: '#bf616a' },
{ tag: [t.angleBracket], color: '#d08770' },
{ tag: [t.attributeName], color: '#ebcb8b' },
{ tag: [t.regexp], color: '#5e81ac' },
{ tag: [t.quote], color: '#b48ead' },
{ tag: [t.string], color: '#a3be8c' },
{
tag: t.link,
color: '#a3be8c',
textDecoration: 'underline',
textUnderlinePosition: 'under',
},
{ tag: [t.url, t.escape, t.special(t.string)], color: '#8fbcbb' },
{ tag: [t.meta], color: '#88c0d0' },
{ tag: [t.monospace], color: '#d8dee9', fontStyle: 'italic' },
{ tag: [t.comment], color: '#4c566a', fontStyle: 'italic' },
{ tag: t.strong, fontWeight: 'bold', color: '#5e81ac' },
{ tag: t.emphasis, fontStyle: 'italic', color: '#5e81ac' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.heading, fontWeight: 'bold', color: '#5e81ac' },
{ tag: t.special(t.heading1), fontWeight: 'bold', color: '#5e81ac' },
{ tag: t.heading1, fontWeight: 'bold', color: '#5e81ac' },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: 'bold',
color: '#5e81ac',
},
{ tag: [t.heading5, t.heading6], color: '#5e81ac' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#d08770' },
{ tag: [t.processingInstruction, t.inserted], color: '#8fbcbb' },
{ tag: [t.contentSeparator], color: '#ebcb8b' },
{ tag: t.invalid, color: '#434c5e', borderBottom: `1px dotted #d30102` },
],
});

View file

@ -1,39 +0,0 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

View file

@ -1,79 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
gutterBackground: '#00252f',
gutterForeground: '#839496',
lineHighlight: '#173541',
};
const c = {
background: '#002B36',
foreground: '#839496',
selection: '#004454AA',
selectionMatch: '#005A6FAA',
cursor: '#D30102',
dropdownBackground: '#00212B',
dropdownBorder: '#2AA19899',
activeLine: '#00cafe11',
matchingBracket: '#073642',
keyword: '#859900',
storage: '#93A1A1',
variable: '#268BD2',
parameter: '#268BD2',
function: '#268BD2',
string: '#2AA198',
constant: '#CB4B16',
type: '#859900',
class: '#268BD2',
number: '#D33682',
comment: '#586E75',
heading: '#268BD2',
invalid: '#DC322F',
regexp: '#DC322F',
tag: '#268BD2',
};
export default createTheme({
theme: 'dark',
settings: {
background: c.background,
foreground: c.foreground,
caret: c.cursor,
selection: c.selection,
selectionMatch: c.selection,
gutterBackground: c.background,
gutterForeground: c.foreground,
gutterBorder: 'transparent',
lineHighlight: c.activeLine,
},
styles: [
{ tag: t.keyword, color: c.keyword },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: c.variable },
{ tag: [t.propertyName], color: c.function },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: c.string },
{ tag: [t.function(t.variableName), t.labelName], color: c.function },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: c.constant },
{ tag: [t.definition(t.name), t.separator], color: c.variable },
{ tag: [t.className], color: c.class },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: c.number },
{ tag: [t.typeName], color: c.type, fontStyle: c.type },
{ tag: [t.operator, t.operatorKeyword], color: c.keyword },
{ tag: [t.url, t.escape, t.regexp, t.link], color: c.regexp },
{ tag: [t.meta, t.comment], color: c.comment },
{ tag: t.tagName, color: c.tag },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: c.heading },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: c.variable },
{ tag: t.invalid, color: c.invalid },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,82 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
// this is slightly different from https://thememirror.net/solarized-light
export const settings = {
light: true,
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
gutterBackground: '#00000010',
gutterForeground: '#657b83',
lineHighlight: '#dfd9c8',
};
const c = {
background: '#FDF6E3',
foreground: '#657B83',
selection: '#EEE8D5',
selectionMatch: '#EEE8D5',
cursor: '#657B83',
dropdownBackground: '#EEE8D5',
dropdownBorder: '#D3AF86',
activeLine: '#3d392d11',
matchingBracket: '#EEE8D5',
keyword: '#859900',
storage: '#586E75',
variable: '#268BD2',
parameter: '#268BD2',
function: '#268BD2',
string: '#2AA198',
constant: '#CB4B16',
type: '#859900',
class: '#268BD2',
number: '#D33682',
comment: '#93A1A1',
heading: '#268BD2',
invalid: '#DC322F',
regexp: '#DC322F',
tag: '#268BD2',
};
export default createTheme({
theme: 'light',
settings: {
background: c.background,
foreground: c.foreground,
caret: c.cursor,
selection: c.selection,
selectionMatch: c.selectionMatch,
gutterBackground: c.background,
gutterForeground: c.foreground,
gutterBorder: 'transparent',
lineHighlight: c.activeLine,
},
styles: [
{ tag: t.keyword, color: c.keyword },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: c.variable },
{ tag: [t.propertyName], color: c.function },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: c.string },
{ tag: [t.function(t.variableName), t.labelName], color: c.function },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: c.constant },
{ tag: [t.definition(t.name), t.separator], color: c.variable },
{ tag: [t.className], color: c.class },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: c.number },
{ tag: [t.typeName], color: c.type, fontStyle: c.type },
{ tag: [t.operator, t.operatorKeyword], color: c.keyword },
{ tag: [t.url, t.escape, t.regexp, t.link], color: c.regexp },
{ tag: [t.meta, t.comment], color: c.comment },
{ tag: t.tagName, color: c.tag },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: c.heading },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: c.variable },
{ tag: t.invalid, color: c.invalid },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,39 +0,0 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c'];
export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[4],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] },
{ tag: [t.attributeName, t.number], color: hex[3] },
{ tag: t.keyword, color: hex[1] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
],
});

View file

@ -1,49 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
lineHighlight: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
// { tag: t.typeName, color: '#f07178' }, // original
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original
{ tag: t.variableName, color: '#c792ea' },
// { tag: t.tagName, color: '#ff5370' }, // original
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});

View file

@ -1,43 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
gutterBackground: '#303841',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#303841',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
gutterBackground: '#303841',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
styles: [
{ tag: t.labelName, color: '#A2A9B5' },
{ tag: [t.meta, t.comment], color: '#A2A9B5' },
{ tag: [t.attributeName, t.keyword], color: '#B78FBA' },
{ tag: t.function(t.variableName), color: '#5AB0B0' },
{ tag: [t.string, t.regexp, t.attributeValue], color: '#99C592' },
{ tag: t.operator, color: '#f47954' },
// { tag: t.moduleKeyword, color: 'red' },
{ tag: [t.tagName, t.modifier], color: '#E35F63' },
{ tag: [t.number, t.definition(t.tagName), t.className, t.definition(t.variableName)], color: '#fbac52' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#E35F63' },
{ tag: t.variableName, color: '#539ac4' },
{ tag: [t.propertyName, t.typeName], color: '#629ccd' },
{ tag: t.propertyName, color: '#36b7b5' },
],
});

View file

@ -1,51 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
// customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: colorB },
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});

View file

@ -1,37 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
],
});

View file

@ -1,42 +0,0 @@
import { EditorView } from '@codemirror/view';
import { syntaxHighlighting } from '@codemirror/language';
import { HighlightStyle } from '@codemirror/language';
export const createTheme = ({ theme, settings, styles }) => {
const _theme = EditorView.theme(
{
'&': {
color: settings.foreground,
backgroundColor: settings.background,
},
'.cm-gutters': {
backgroundColor: settings.gutterBackground,
color: settings.gutterForeground,
//borderRightColor: settings.gutterBorder
},
'.cm-content': {
caretColor: settings.caret,
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: settings.caret,
},
'.cm-activeLineGutter': {
// color: settings.gutterActiveForeground
backgroundColor: settings.lineHighlight,
},
'.cm-activeLine': {
backgroundColor: settings.lineHighlight,
},
'&.cm-focused .cm-selectionBackground, & .cm-line::selection, & .cm-selectionLayer .cm-selectionBackground, .cm-content ::selection':
{
background: settings.selection + ' !important',
},
'& .cm-selectionMatch': {
backgroundColor: settings.selectionMatch,
},
},
{ dark: theme === 'dark' },
);
const highlightStyle = HighlightStyle.define(styles);
return [_theme, syntaxHighlighting(highlightStyle)];
};

View file

@ -1,52 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
gutterBackground: '#24283b',
gutterForeground: '#7982a9',
gutterBorder: 'transparent',
lineHighlight: '#292e42',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#24283b',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#343b5f',
gutterBackground: '#24283b',
gutterForeground: '#7982a9',
gutterBorder: 'transparent',
lineHighlight: '#292e427a',
},
styles: [
{ tag: t.keyword, color: '#bb9af7' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#c0caf5' },
{ tag: [t.propertyName], color: '#7aa2f7' },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#9ece6a' },
{ tag: [t.function(t.variableName), t.labelName], color: '#7aa2f7' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#bb9af7' },
{ tag: [t.definition(t.name), t.separator], color: '#c0caf5' },
{ tag: [t.className], color: '#c0caf5' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#ff9e64' },
{ tag: [t.typeName], color: '#2ac3de', fontStyle: '#2ac3de' },
{ tag: [t.operator, t.operatorKeyword], color: '#bb9af7' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#b4f9f8' },
{ tag: [t.meta, t.comment], color: '#565f89' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#89ddff' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c0caf5' },
{ tag: t.invalid, color: '#ff5370' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,52 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
gutterBackground: '#1a1b26',
gutterForeground: '#787c99',
gutterBorder: 'transparent',
lineHighlight: '#1e202e',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#1a1b26',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
gutterBackground: '#1a1b26',
gutterForeground: '#787c99',
gutterBorder: 'transparent',
lineHighlight: '#474b6611',
},
styles: [
{ tag: t.keyword, color: '#bb9af7' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#c0caf5' },
{ tag: [t.propertyName], color: '#7aa2f7' },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#9ece6a' },
{ tag: [t.function(t.variableName), t.labelName], color: '#7aa2f7' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#bb9af7' },
{ tag: [t.definition(t.name), t.separator], color: '#c0caf5' },
{ tag: [t.className], color: '#c0caf5' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#ff9e64' },
{ tag: [t.typeName], color: '#0db9d7' },
{ tag: [t.operator, t.operatorKeyword], color: '#bb9af7' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#b4f9f8' },
{ tag: [t.meta, t.comment], color: '#444b6a' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#89ddff' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c0caf5' },
{ tag: t.invalid, color: '#ff5370' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,53 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
gutterBackground: '#e1e2e7',
gutterForeground: '#3760bf',
gutterBorder: 'transparent',
lineHighlight: '#5f5faf11',
};
export default createTheme({
theme: 'light',
settings: {
background: '#e1e2e7',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
gutterBackground: '#e1e2e7',
gutterForeground: '#3760bf',
gutterBorder: 'transparent',
lineHighlight: '#5f5faf11',
},
styles: [
{ tag: t.keyword, color: '#007197' },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: '#3760bf' },
{ tag: [t.propertyName], color: '#3760bf' },
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#587539' },
{ tag: [t.function(t.variableName), t.labelName], color: '#3760bf' },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#3760bf' },
{ tag: [t.definition(t.name), t.separator], color: '#3760bf' },
{ tag: [t.className], color: '#3760bf' },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#b15c00' },
{ tag: [t.typeName], color: '#007197', fontStyle: '#007197' },
{ tag: [t.operator, t.operatorKeyword], color: '#007197' },
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#587539' },
{ tag: [t.meta, t.comment], color: '#848cb5' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: '#b15c00' },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#3760bf' },
{ tag: t.invalid, color: '#f52a65' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
],
});

View file

@ -1,80 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#fff',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
lineHighlight: '#ffffff0f',
gutterBackground: '#1e1e1e',
gutterForeground: '#838383',
gutterActiveForeground: '#fff',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#1e1e1e',
foreground: '#fff',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
lineHighlight: '#ffffff0f',
gutterBackground: '#1e1e1e',
gutterForeground: '#838383',
gutterActiveForeground: '#fff',
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
},
styles: [
{
tag: [
t.keyword,
t.operatorKeyword,
t.modifier,
t.color,
t.constant(t.name),
t.standard(t.name),
t.standard(t.tagName),
t.special(t.brace),
t.atom,
t.bool,
t.special(t.variableName),
],
color: '#569cd6',
},
{ tag: [t.controlKeyword, t.moduleKeyword], color: '#c586c0' },
{
tag: [
t.name,
t.deleted,
t.character,
t.macroName,
t.propertyName,
t.variableName,
t.labelName,
t.definition(t.name),
],
color: '#9cdcfe',
},
{ tag: t.heading, fontWeight: 'bold', color: '#9cdcfe' },
{
tag: [t.typeName, t.className, t.tagName, t.number, t.changed, t.annotation, t.self, t.namespace],
color: '#4ec9b0',
},
{ tag: [t.function(t.variableName), t.function(t.propertyName)], color: '#dcdcaa' },
{ tag: [t.number], color: '#b5cea8' },
{ tag: [t.operator, t.punctuation, t.separator, t.url, t.escape, t.regexp], color: '#d4d4d4' },
{ tag: [t.regexp], color: '#d16969' },
{ tag: [t.special(t.string), t.processingInstruction, t.string, t.inserted], color: '#ce9178' },
{ tag: [t.angleBracket], color: '#808080' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: [t.meta, t.comment], color: '#6a9955' },
{ tag: t.link, color: '#6a9955', textDecoration: 'underline' },
{ tag: t.invalid, color: '#ff0000' },
],
});

View file

@ -1,81 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#ffffff',
lineBackground: '#ffffff50',
foreground: '#383a42',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
lineHighlight: '#99999926',
gutterBackground: '#fff',
gutterForeground: '#237893',
gutterActiveForeground: '#0b216f',
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
};
export default createTheme({
theme: 'light',
settings: {
background: '#ffffff',
foreground: '#383a42',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
lineHighlight: '#99999926',
gutterBackground: '#fff',
gutterForeground: '#237893',
gutterActiveForeground: '#0b216f',
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
},
styles: [
{
tag: [
t.keyword,
t.operatorKeyword,
t.modifier,
t.color,
t.constant(t.name),
t.standard(t.name),
t.standard(t.tagName),
t.special(t.brace),
t.atom,
t.bool,
t.special(t.variableName),
],
color: '#0000ff',
},
{ tag: [t.moduleKeyword, t.controlKeyword], color: '#af00db' },
{
tag: [
t.name,
t.deleted,
t.character,
t.macroName,
t.propertyName,
t.variableName,
t.labelName,
t.definition(t.name),
],
color: '#0070c1',
},
{ tag: t.heading, fontWeight: 'bold', color: '#0070c1' },
{
tag: [t.typeName, t.className, t.tagName, t.number, t.changed, t.annotation, t.self, t.namespace],
color: '#267f99',
},
{ tag: [t.function(t.variableName), t.function(t.propertyName)], color: '#795e26' },
{ tag: [t.number], color: '#098658' },
{ tag: [t.operator, t.punctuation, t.separator, t.url, t.escape, t.regexp], color: '#383a42' },
{ tag: [t.regexp], color: '#af00db' },
{ tag: [t.special(t.string), t.processingInstruction, t.string, t.inserted], color: '#a31515' },
{ tag: [t.angleBracket], color: '#383a42' },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: [t.meta, t.comment], color: '#008000' },
{ tag: t.link, color: '#4078f2', textDecoration: 'underline' },
{ tag: t.invalid, color: '#e45649' },
],
});

View file

@ -1,39 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
],
});

View file

@ -1,38 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#AFAFAF',
lineHighlight: '#EDF4FF',
};
export default createTheme({
theme: 'light',
settings: {
background: '#fff',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#AFAFAF',
lineHighlight: '#d5e6ff69',
},
styles: [
{ tag: [t.comment, t.quote], color: '#707F8D' },
{ tag: [t.typeName, t.typeOperator], color: '#aa0d91' },
{ tag: [t.keyword], color: '#aa0d91', fontWeight: 'bold' },
{ tag: [t.string, t.meta], color: '#D23423' },
{ tag: [t.name], color: '#032f62' },
{ tag: [t.typeName], color: '#522BB2' },
{ tag: [t.variableName], color: '#23575C' },
{ tag: [t.definition(t.variableName)], color: '#327A9E' },
{ tag: [t.regexp, t.link], color: '#0e0eff' },
],
});

View file

@ -1,79 +0,0 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
let ctrlDown = false;
if (typeof window !== 'undefined') {
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
}
export const strudelTooltip = hoverTooltip(
(view, pos, side) => {
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
if (!ctrlDown) {
return null;
}
let { from, to, text } = view.state.doc.lineAt(pos);
let start = pos,
end = pos;
while (start > from && /\w/.test(text[start - from - 1])) {
start--;
}
while (end < to && /\w/.test(text[end - from])) {
end++;
}
if ((start == pos && side < 0) || (end == pos && side > 0)) {
return null;
}
let word = text.slice(start - from, end - from);
// Get entry from Strudel documentation
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!doc) {
return null;
}
entry = getSynonymDoc(doc, word);
}
return {
pos: start,
end,
above: false,
arrow: true,
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete(entry);
dom.appendChild(ac);
return { dom };
},
};
},
{ hoverTime: 10 },
);
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);

View file

@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

View file

@ -1,142 +0,0 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
import { Pattern } from '@strudel/core';
export const addWidget = StateEffect.define({
map: ({ from, to }, change) => {
return { from: change.mapPos(from), to: change.mapPos(to) };
},
});
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: addWidget.of(widgets) });
};
function getWidgets(widgetConfigs) {
return (
widgetConfigs
// codemirror throws an error if we don't sort
.sort((a, b) => a.to - b.to)
.map((widgetConfig) => {
return Decoration.widget({
widget: new BlockWidget(widgetConfig),
side: 0,
block: true,
}).range(widgetConfig.to);
})
);
}
const widgetField = StateField.define(
/* <DecorationSet> */ {
create() {
return Decoration.none;
},
update(widgets, tr) {
widgets = widgets.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addWidget)) {
try {
widgets = widgets.update({
filter: () => false,
add: getWidgets(e.value),
});
} catch (error) {
console.log('err', error);
}
}
}
return widgets;
},
provide: (f) => EditorView.decorations.from(f),
},
);
const widgetElements = {};
export function setWidget(id, el) {
widgetElements[id] = el;
el.id = id;
}
export class BlockWidget extends WidgetType {
constructor(widgetConfig) {
super();
this.widgetConfig = widgetConfig;
}
eq() {
return true;
}
toDOM() {
const id = getWidgetID(this.widgetConfig);
const el = widgetElements[id];
return el;
}
ignoreEvent(e) {
return true;
}
}
export const widgetPlugin = [widgetField];
// widget implementer API to create a new widget type
export function registerWidget(type, fn) {
registerWidgetType(type);
if (fn) {
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
// fn is expected to create a dom element and call setWidget(id, el);
// fn should also return the pattern
return fn(id, options, this);
};
}
}
// wire up @strudel/draw functions
function getCanvasWidget(id, options = {}) {
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
let canvas = document.getElementById(id) || document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
setWidget(id, canvas);
return canvas;
}
registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
});
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).spiral({ ...options, ctx, id });
});
registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).scope({ ...options, ctx, id });
});
registerWidget('_pitchwheel', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
registerWidget('_spectrum', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id });
});

View file

@ -4,34 +4,34 @@
// import createClock from './zyklus.mjs';
function getTime() {
const seconds = performance.now() * 0.001;
return seconds;
const seconds = performance.now() * 0.001
return seconds
// return Math.round(seconds * precision) / precision;
}
let num_cycles_at_cps_change = 0;
let num_ticks_since_cps_change = 0;
let num_seconds_at_cps_change = 0;
let cps = 0.5;
let num_cycles_at_cps_change = 0
let num_ticks_since_cps_change = 0
let num_seconds_at_cps_change = 0
let cps = 0.5
// {id: {started: boolean}}
const clients = new Map();
const duration = 0.1;
const channel = new BroadcastChannel('strudeltick');
const clients = new Map()
const duration = 0.1
const channel = new BroadcastChannel('strudeltick')
const sendMessage = (type, payload) => {
channel.postMessage({ type, payload });
};
channel.postMessage({ type, payload })
}
const sendTick = (phase, duration, tick, time) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * cps;
const end = begin + eventLength;
const cycle = begin + secondsSinceLastTick * cps;
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration
const tickdeadline = phase - time
const lastTick = time + tickdeadline
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change
const secondsSinceLastTick = time - lastTick - duration
const eventLength = duration * cps
const end = begin + eventLength
const cycle = begin + secondsSinceLastTick * cps
sendMessage('tick', {
begin,
@ -39,79 +39,79 @@ const sendTick = (phase, duration, tick, time) => {
cps,
time,
cycle,
});
num_ticks_since_cps_change++;
};
})
num_ticks_since_cps_change++
}
//create clock method from zyklus
const clock = createClock(getTime, sendTick, duration);
let started = false;
const clock = createClock(getTime, sendTick, duration)
let started = false
const startClock = (id) => {
clients.set(id, { started: true });
clients.set(id, { started: true })
if (started) {
return;
return
}
clock.start();
started = true;
};
clock.start()
started = true
}
const stopClock = async (id) => {
clients.set(id, { started: false });
clients.set(id, { started: false })
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
const otherClientStarted = Array.from(clients.values()).some((c) => c.started)
//dont stop the clock if other instances are running...
if (!started || otherClientStarted) {
return;
return
}
clock.stop();
setCycle(0);
started = false;
};
clock.stop()
setCycle(0)
started = false
}
const setCycle = (cycle) => {
num_ticks_since_cps_change = 0;
num_cycles_at_cps_change = cycle;
};
num_ticks_since_cps_change = 0
num_cycles_at_cps_change = cycle
}
const processMessage = (message) => {
const { type, payload } = message;
const { type, payload } = message
switch (type) {
case 'cpschange': {
if (payload.cps !== cps) {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
cps = payload.cps;
num_ticks_since_cps_change = 0;
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change
cps = payload.cps
num_ticks_since_cps_change = 0
}
break;
break
}
case 'setcycle': {
setCycle(payload.cycle);
break;
setCycle(payload.cycle)
break
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
startClock(message.id)
} else {
stopClock(message.id);
stopClock(message.id)
}
break;
break
}
}
};
}
self.onconnect = function (e) {
// the incoming port
const port = e.ports[0];
const port = e.ports[0]
port.addEventListener('message', function (e) {
processMessage(e.data);
});
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
};
processMessage(e.data)
})
port.start() // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
}
// used to consistently schedule events, for use in a service worker - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/clockworker.mjs>
function createClock(
@ -119,43 +119,43 @@ function createClock(
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
overlap = 0.1 // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
let tick = 0 // counts callbacks
let phase = 0 // next callback time
let precision = 10 ** 4 // used to round phase
let minLatency = 0.01
const setDuration = (setter) => (duration = setter(duration))
overlap = overlap || interval / 2
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
const t = getTime()
const lookahead = t + interval + overlap // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
phase = t + minLatency
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick, t);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
phase = Math.round(phase * precision) / precision
phase >= t && callback(phase, duration, tick, t)
phase < t && console.log('TOO LATE', phase) // what if latency is added from outside?
phase += duration // increment phase by duration
tick++
}
};
let intervalID;
}
let intervalID
const start = () => {
clear(); // just in case start was called more than once
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
clear() // just in case start was called more than once
onTick()
intervalID = setInterval(onTick, interval * 1000)
}
const clear = () => intervalID !== undefined && clearInterval(intervalID)
const pause = () => clear()
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
tick = 0
phase = 0
clear()
}
const getPhase = () => phase
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }
}

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import createClock from './zyklus.mjs';
import { errorLogger, logger } from './logger.mjs';
import createClock from './zyklus.mjs'
import { errorLogger, logger } from './logger.mjs'
export class Cyclist {
constructor({
@ -19,122 +19,126 @@ export class Cyclist {
clearInterval,
beforeStart,
}) {
this.started = false;
this.beforeStart = beforeStart;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
this.started = false
this.beforeStart = beforeStart
this.cps = 0.5
this.num_ticks_since_cps_change = 0
this.lastTick = 0 // absolute time when last tick (clock callback) happened
this.lastBegin = 0 // query begin of last tick
this.lastEnd = 0 // query end of last tick
this.getTime = getTime // get absolute time
this.num_cycles_at_cps_change = 0
this.seconds_at_cps_change // clock phase when cps was changed
this.onToggle = onToggle
this.latency = latency // fixed trigger time offset
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, _, t) => {
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
this.num_cycles_at_cps_change = this.lastEnd
this.seconds_at_cps_change = phase
}
this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
this.num_ticks_since_cps_change++
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps
try {
const begin = this.lastEnd;
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
const begin = this.lastEnd
this.lastBegin = begin
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change
this.lastEnd = end
this.lastTick = phase
if (phase < t) {
// avoid querying haps that are in the past anyway
console.log(`skip query: too late`);
return;
console.log(`skip query: too late`)
return
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' })
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps;
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps +
this.seconds_at_cps_change +
latency
const duration = hap.duration / this.cps
// the following line is dumb and only here for backwards compatibility
// see https://codeberg.org/uzu/strudel/pulls/1004
const deadline = targetTime - phase;
const deadline = targetTime - phase
// this onTrigger has another signature
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
onTrigger?.(hap, deadline, duration, this.cps, targetTime)
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps;
this.num_ticks_since_cps_change = 0;
this.cps = hap.value.cps
this.num_ticks_since_cps_change = 0
}
}
});
})
} catch (e) {
errorLogger(e);
onError?.(e);
errorLogger(e)
onError?.(e)
}
},
interval, // duration of each cycle
0.1,
0.1,
setInterval,
clearInterval,
);
clearInterval
)
}
now() {
if (!this.started) {
return 0;
return 0
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration
return this.lastBegin + secondsSinceLastTick * this.cps // + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);
this.started = v
this.onToggle?.(v)
}
async start() {
await this.beforeStart?.();
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0;
await this.beforeStart?.()
this.num_ticks_since_cps_change = 0
this.num_cycles_at_cps_change = 0
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
throw new Error('Scheduler: no pattern set! call .setPattern first.')
}
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
logger('[cyclist] start')
this.clock.start()
this.setStarted(true)
}
pause() {
logger('[cyclist] pause');
this.clock.pause();
this.setStarted(false);
logger('[cyclist] pause')
this.clock.pause()
this.setStarted(false)
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
logger('[cyclist] stop')
this.clock.stop()
this.lastEnd = 0
this.setStarted(false)
}
async setPattern(pat, autostart = false) {
this.pattern = pat;
this.pattern = pat
if (autostart && !this.started) {
await this.start();
await this.start()
}
}
setCps(cps = 0.5) {
if (this.cps === cps) {
return;
return
}
this.cps = cps;
this.num_ticks_since_cps_change = 0;
this.cps = cps
this.num_ticks_since_cps_change = 0
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
const onsets = haps.filter((h) => h.hasOnset())
console.log(
`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`
)
}
}

View file

@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction, { gcd } from './fraction.mjs';
import Fraction, { gcd } from './fraction.mjs'
/**
* Intended for a debugging, drawLine renders the pattern as a string, where each character represents the same time span.
@ -24,39 +24,39 @@ import Fraction, { gcd } from './fraction.mjs';
* silence;
*/
function drawLine(pat, chars = 60) {
let cycle = 0;
let pos = Fraction(0);
let lines = [''];
let emptyLine = ''; // this will be the "reference" empty line, which will be copied into extra lines
let cycle = 0
let pos = Fraction(0)
let lines = ['']
let emptyLine = '' // this will be the "reference" empty line, which will be copied into extra lines
while (lines[0].length < chars) {
const haps = pat.queryArc(cycle, cycle + 1);
const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration);
const charFraction = gcd(...durations);
const totalSlots = charFraction.inverse(); // number of character slots for the current cycle
lines = lines.map((line) => line + '|'); // add pipe character before each cycle
emptyLine += '|';
const haps = pat.queryArc(cycle, cycle + 1)
const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration)
const charFraction = gcd(...durations)
const totalSlots = charFraction.inverse() // number of character slots for the current cycle
lines = lines.map((line) => line + '|') // add pipe character before each cycle
emptyLine += '|'
for (let i = 0; i < totalSlots; i++) {
const [begin, end] = [pos, pos.add(charFraction)];
const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end));
const missingLines = matches.length - lines.length;
const [begin, end] = [pos, pos.add(charFraction)]
const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end))
const missingLines = matches.length - lines.length
if (missingLines > 0) {
lines = lines.concat(Array(missingLines).fill(emptyLine));
lines = lines.concat(Array(missingLines).fill(emptyLine))
}
lines = lines.map((line, i) => {
const hap = matches[i];
const hap = matches[i]
if (hap) {
const isOnset = hap.whole.begin.eq(begin);
const char = isOnset ? '' + hap.value : '-';
return line + char;
const isOnset = hap.whole.begin.eq(begin)
const char = isOnset ? '' + hap.value : '-'
return line + char
}
return line + '.';
});
emptyLine += '.';
pos = pos.add(charFraction);
return line + '.'
})
emptyLine += '.'
pos = pos.add(charFraction)
}
cycle++;
cycle++
}
return lines.join('\n');
return lines.join('\n')
}
export default drawLine;
export default drawLine

View file

@ -10,46 +10,46 @@ https://rohandrape.net/?t=hmt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs'
import { rotate, flatten, splitAt, zipWith } from './util.mjs'
import Fraction, { lcm } from './fraction.mjs'
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_xs, __xs] = splitAt(offs, xs);
const [ons, offs] = n
const [xs, ys] = x
const [_xs, __xs] = splitAt(offs, xs)
return [
[offs, ons - offs],
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
];
};
]
}
const right = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_ys, __ys] = splitAt(ons, ys);
const [ons, offs] = n
const [xs, ys] = x
const [_ys, __ys] = splitAt(ons, ys)
const result = [
[ons, offs - ons],
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
];
return result;
};
]
return result
}
const _bjork = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
const [ons, offs] = n
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)))
}
export const bjork = function (ons, steps) {
const inverted = ons < 0;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
const result = _bjork([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
};
const inverted = ons < 0
const absOns = Math.abs(ons)
const offs = steps - absOns
const ones = Array(absOns).fill([1])
const zeros = Array(offs).fill([0])
const result = _bjork([absOns, offs], [ones, zeros])
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]))
return inverted ? pattern.map((x) => 1 - x) : pattern
}
/**
* Changes the structure of the pattern to form an Euclidean rhythm.
@ -128,28 +128,31 @@ export const bjork = function (ons, steps) {
*/
const _euclidRot = function (pulses, steps, rotation) {
const b = bjork(pulses, steps);
const b = bjork(pulses, steps)
if (rotation) {
return rotate(b, -rotation);
return rotate(b, -rotation)
}
return b;
};
return b
}
export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
return pat.struct(_euclidRot(pulses, steps, 0))
})
export const e = register('e', function (euc, pat) {
if (!Array.isArray(euc)) {
euc = [euc];
euc = [euc]
}
const [pulses, steps = pulses, rot = 0] = euc;
return pat.struct(_euclidRot(pulses, steps, rot));
});
const [pulses, steps = pulses, rot = 0] = euc
return pat.struct(_euclidRot(pulses, steps, rot))
})
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation));
});
export const { euclidrot, euclidRot } = register(
['euclidrot', 'euclidRot'],
function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation))
}
)
/**
* Similar to `euclid`, but each pulse is held until the next pulse,
@ -166,20 +169,20 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
return silence
}
const bin_pat = _euclidRot(pulses, steps, 0);
const bin_pat = _euclidRot(pulses, steps, 0)
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
};
.map((s) => [s.length + 1, true])
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps))
}
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
return _euclidLegato(pulses, steps, 0, pat);
});
return _euclidLegato(pulses, steps, 0, pat)
})
/**
* Similar to `euclid`, but each pulse is held until the next pulse,
@ -193,9 +196,12 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* @example
* note("c3").euclidLegatoRot(3,5,2)
*/
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
export const euclidLegatoRot = register(
['euclidLegatoRot'],
function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat)
}
)
/**
* A 'euclid' variant with an additional parameter that morphs the resulting
@ -215,7 +221,10 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* sound("hh").euclidish(7,12,sine.slow(8))
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
export const { euclidish, eish } = register(
['euclidish', 'eish'],
function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc)
return pat.struct(morphed).setSteps(steps)
}
)

View file

@ -4,51 +4,62 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const strudelScope = {};
export const strudelScope = {}
export const evalScope = async (...args) => {
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
const results = await Promise.allSettled(args)
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value)
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason)
}
});
})
// Object.assign(globalThis, ...modules);
// below is a fix for above commented out line
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
modules.forEach((module) => {
Object.entries(module).forEach(([name, value]) => {
globalThis[name] = value;
strudelScope[name] = value;
});
});
return modules;
};
// globalThis[name] = value
strudelScope[name] = value
})
})
return modules
}
export const addToScope = (...obj) => {
obj.forEach((o) => {
Object.entries(o).forEach(([name, value]) => {
strudelScope[name] = value
})
})
}
function safeEval(str, options = {}) {
const { wrapExpression = true, wrapAsync = true } = options;
const { wrapExpression = true, wrapAsync = true } = options
if (wrapExpression) {
str = `{${str}}`;
str = `{${str}}`
}
if (wrapAsync) {
str = `(async ()=>${str})()`;
str = `(async ()=>${str})()`
}
const body = `"use strict";return (${str})`;
return Function(body)();
const body = `"use strict";return (${str})`
console.log(strudelScope)
const scopedKeys = Object.keys(strudelScope)
const scopedValues = Object.values(strudelScope)
return Function(...scopedKeys, body)(...scopedValues)
}
export const evaluate = async (code, transpiler, transpilerOptions) => {
let meta = {};
let meta = {}
if (transpiler) {
// transform syntactically correct js code to semantically usable code
const transpiled = transpiler(code, transpilerOptions);
code = transpiled.output;
meta = transpiled;
const transpiled = transpiler(code, transpilerOptions)
code = transpiled.output
meta = transpiled
}
// if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler };
let evaluated = await safeEval(code, options);
return { mode: 'javascript', pattern: evaluated, meta };
};
const options = { wrapExpression: !!transpiler }
let evaluated = await safeEval(code, options)
return { mode: 'javascript', pattern: evaluated, meta }
}

View file

@ -4,91 +4,91 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from 'fraction.js';
import { TimeSpan } from './timespan.mjs';
import { removeUndefineds } from './util.mjs';
import Fraction from 'fraction.js'
import { TimeSpan } from './timespan.mjs'
import { removeUndefineds } from './util.mjs'
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
return this.floor();
};
return this.floor()
}
// Returns the start of the next cycle.
Fraction.prototype.nextSam = function () {
return this.sam().add(1);
};
return this.sam().add(1)
}
// Returns a TimeSpan representing the begin and end of the Time value's cycle
Fraction.prototype.wholeCycle = function () {
return new TimeSpan(this.sam(), this.nextSam());
};
return new TimeSpan(this.sam(), this.nextSam())
}
// The position of a time value relative to the start of its cycle.
Fraction.prototype.cyclePos = function () {
return this.sub(this.sam());
};
return this.sub(this.sam())
}
Fraction.prototype.lt = function (other) {
return this.compare(other) < 0;
};
return this.compare(other) < 0
}
Fraction.prototype.gt = function (other) {
return this.compare(other) > 0;
};
return this.compare(other) > 0
}
Fraction.prototype.lte = function (other) {
return this.compare(other) <= 0;
};
return this.compare(other) <= 0
}
Fraction.prototype.gte = function (other) {
return this.compare(other) >= 0;
};
return this.compare(other) >= 0
}
Fraction.prototype.eq = function (other) {
return this.compare(other) == 0;
};
return this.compare(other) == 0
}
Fraction.prototype.ne = function (other) {
return this.compare(other) != 0;
};
return this.compare(other) != 0
}
Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other;
};
return this.gt(other) ? this : other
}
Fraction.prototype.maximum = function (...others) {
others = others.map((x) => new Fraction(x));
return others.reduce((max, other) => other.max(max), this);
};
others = others.map((x) => new Fraction(x))
return others.reduce((max, other) => other.max(max), this)
}
Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other;
};
return this.lt(other) ? this : other
}
Fraction.prototype.mulmaybe = function (other) {
return other !== undefined ? this.mul(other) : undefined;
};
return other !== undefined ? this.mul(other) : undefined
}
Fraction.prototype.divmaybe = function (other) {
return other !== undefined ? this.div(other) : undefined;
};
return other !== undefined ? this.div(other) : undefined
}
Fraction.prototype.addmaybe = function (other) {
return other !== undefined ? this.add(other) : undefined;
};
return other !== undefined ? this.add(other) : undefined
}
Fraction.prototype.submaybe = function (other) {
return other !== undefined ? this.sub(other) : undefined;
};
return other !== undefined ? this.sub(other) : undefined
}
Fraction.prototype.show = function (/* excludeWhole = false */) {
// return this.toFraction(excludeWhole);
return this.s * this.n + '/' + this.d;
};
return this.s * this.n + '/' + this.d
}
Fraction.prototype.or = function (other) {
return this.eq(0) ? other : this;
};
return this.eq(0) ? other : this
}
const fraction = (n) => {
if (typeof n === 'number') {
@ -102,35 +102,36 @@ const fraction = (n) => {
*/
// n = String(n); // this is actually faster but imprecise...
}
return Fraction(n);
};
return Fraction(n)
}
export const gcd = (...fractions) => {
fractions = removeUndefineds(fractions);
fractions = removeUndefineds(fractions)
if (fractions.length === 0) {
return undefined;
return undefined
}
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
};
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1))
}
export const lcm = (...fractions) => {
fractions = removeUndefineds(fractions);
fractions = removeUndefineds(fractions)
if (fractions.length === 0) {
return undefined;
return undefined
}
const x = fractions.pop();
const x = fractions.pop()
return fractions.reduce(
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
x,
);
};
(lcm, fraction) =>
lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction),
x
)
}
export const isFraction = (x) => x instanceof Fraction;
export const isFraction = (x) => x instanceof Fraction
fraction._original = Fraction;
fraction._original = Fraction
export default fraction;
export default fraction
// "If you concern performance, cache Fraction.js objects and pass arrays/objects.“
// -> tested memoized version, but it's slower than unmemoized, even with repeated evaluation

View file

@ -3,8 +3,10 @@ hap.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/hap.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
import Fraction from './fraction.mjs'
import { stringifyValues } from './util.mjs'
/* eslint-disable @typescript-eslint/no-this-alias */
export class Hap {
/*
@ -23,91 +25,91 @@ export class Hap {
*/
constructor(whole, part, value, context = {}, stateful = false) {
this.whole = whole;
this.part = part;
this.value = value;
this.context = context;
this.stateful = stateful;
this.whole = whole
this.part = part
this.value = value
this.context = context
this.stateful = stateful
if (stateful) {
console.assert(typeof this.value === 'function', 'Stateful values must be functions');
console.assert(typeof this.value === 'function', 'Stateful values must be functions')
}
}
get duration() {
let duration;
let duration
if (typeof this.value?.duration === 'number') {
duration = Fraction(this.value.duration);
duration = Fraction(this.value.duration)
} else {
duration = this.whole.end.sub(this.whole.begin);
duration = this.whole.end.sub(this.whole.begin)
}
if (typeof this.value?.clip === 'number') {
return duration.mul(this.value.clip);
return duration.mul(this.value.clip)
}
return duration;
return duration
}
get endClipped() {
return this.whole.begin.add(this.duration);
return this.whole.begin.add(this.duration)
}
isActive(currentTime) {
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
return this.whole.begin <= currentTime && this.endClipped >= currentTime
}
isInPast(currentTime) {
return currentTime > this.endClipped;
return currentTime > this.endClipped
}
isInNearPast(margin, currentTime) {
return currentTime - margin <= this.endClipped;
return currentTime - margin <= this.endClipped
}
isInFuture(currentTime) {
return currentTime < this.whole.begin;
return currentTime < this.whole.begin
}
isInNearFuture(margin, currentTime) {
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin
}
isWithinTime(min, max) {
return this.whole.begin <= max && this.endClipped >= min;
return this.whole.begin <= max && this.endClipped >= min
}
wholeOrPart() {
return this.whole ? this.whole : this.part;
return this.whole ? this.whole : this.part
}
withSpan(func) {
// Returns a new hap with the function f applies to the hap timespan.
const whole = this.whole ? func(this.whole) : undefined;
return new Hap(whole, func(this.part), this.value, this.context);
const whole = this.whole ? func(this.whole) : undefined
return new Hap(whole, func(this.part), this.value, this.context)
}
withValue(func) {
// Returns a new hap with the function f applies to the hap value.
return new Hap(this.whole, this.part, func(this.value), this.context);
return new Hap(this.whole, this.part, func(this.value), this.context)
}
hasOnset() {
// Test whether the hap contains the onset, i.e that
// the beginning of the part is the same as that of the whole timespan."""
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
return this.whole != undefined && this.whole.begin.equals(this.part.begin)
}
hasTag(tag) {
return this.context.tags?.includes(tag);
return this.context.tags?.includes(tag)
}
resolveState(state) {
if (this.stateful && this.hasOnset()) {
console.log('stateful');
const func = this.value;
const [newState, newValue] = func(state);
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)];
console.log('stateful')
const func = this.value
const [newState, newValue] = func(state)
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)]
}
return [state, this];
return [state, this]
}
spanEquals(other) {
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole);
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole)
}
equals(other) {
@ -116,7 +118,7 @@ export class Hap {
this.part.equals(other.part) &&
// TODO would == be better ??
this.value === other.value
);
)
}
show(compact = false) {
@ -125,40 +127,45 @@ export class Hap {
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value;
var spans = '';
: this.value
var spans = ''
if (this.whole == undefined) {
spans = '~' + this.part.show;
spans = '~' + this.part.show
} else {
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
var is_whole =
this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end)
if (!this.whole.begin.equals(this.part.begin)) {
spans = this.whole.begin.show() + ' ⇜ ';
spans = this.whole.begin.show() + ' ⇜ '
}
if (!is_whole) {
spans += '(';
spans += '('
}
spans += this.part.show();
spans += this.part.show()
if (!is_whole) {
spans += ')';
spans += ')'
}
if (!this.whole.end.equals(this.part.end)) {
spans += ' ⇝ ' + this.whole.end.show();
spans += ' ⇝ ' + this.whole.end.show()
}
}
return '[ ' + spans + ' | ' + value + ' ]';
return '[ ' + spans + ' | ' + value + ' ]'
}
showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`
}
combineContext(b) {
const a = this;
return { ...a.context, ...b.context, locations: (a.context.locations || []).concat(b.context.locations || []) };
const a = this
return {
...a.context,
...b.context,
locations: (a.context.locations || []).concat(b.context.locations || []),
}
}
setContext(context) {
return new Hap(this.whole, this.part, this.value, context);
return new Hap(this.whole, this.part, this.value, context)
}
ensureObjectValue() {
@ -169,10 +176,10 @@ export class Hap {
if (typeof this.value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
'error',
);
'error'
)
}
}
}
export default Hap;
export default Hap

View file

@ -4,37 +4,29 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as controls from './controls.mjs'; // legacy
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls, createClock };
export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
export * from './pick.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
import * as controls from './controls.mjs' // legacy
export * from './euclid.mjs'
import Fraction from './fraction.mjs'
import createClock from './zyklus.mjs'
import { logger } from './logger.mjs'
export { Fraction, controls, createClock }
export * from './controls.mjs'
export * from './hap.mjs'
export * from './pattern.mjs'
export * from './signal.mjs'
export * from './pick.mjs'
export * from './state.mjs'
export * from './timespan.mjs'
export * from './util.mjs'
export * from './speak.mjs'
export * from './evaluate.mjs'
export * from './repl.mjs'
export * from './cyclist.mjs'
export * from './logger.mjs'
export * from './time.mjs'
export * from './ui.mjs'
export { default as drawLine } from './drawLine.mjs'
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
export const version = p.version; */
logger('🌀 @strudel/core loaded 🌀');
if (globalThis._strudelLoaded) {
console.warn(
`@strudel/core was loaded more than once...
This might happen when you have multiple versions of strudel installed.
Please check with "npm ls @strudel/core".`,
);
}
globalThis._strudelLoaded = true;
logger('🌀 @strudel/core loaded 🌀')

View file

@ -1,24 +1,24 @@
export const logKey = 'strudel.log';
export const logKey = 'strudel.log'
let debounce = 1000,
lastMessage,
lastTime;
lastTime
export function errorLogger(e, origin = 'cyclist') {
if (process.env.NODE_ENV === 'development') {
console.error(e);
console.error(e)
}
logger(`[${origin}] error: ${e.message}`);
logger(`[${origin}] error: ${e.message}`)
}
export function logger(message, type, data = {}) {
let t = performance.now();
let t = performance.now()
if (lastMessage === message && t - lastTime < debounce) {
return;
return
}
lastMessage = message;
lastTime = t;
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
lastMessage = message
lastTime = t
console.log(`%c${message}`)
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
@ -27,9 +27,9 @@ export function logger(message, type, data = {}) {
type,
data,
},
}),
);
})
)
}
}
logger.key = logKey;
logger.key = logKey

View file

@ -4,102 +4,104 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs';
import { logger } from './logger.mjs'
import { ClockCollator, cycleToSeconds } from './util.mjs'
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
this.started = false
this.cps = 0.5
this.getTime = getTime // get absolute time
this.time_at_last_tick_message = 0
// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
this.collator = new ClockCollator({ getTargetClockTime: getTime });
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
this.collator = new ClockCollator({ getTargetClockTime: getTime })
this.onToggle = onToggle
this.latency = 0.1 // fixed trigger time offset
this.cycle = 0
this.id = Math.round(Date.now() * Math.random())
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url))
this.worker.port.start()
this.channel = new BroadcastChannel('strudeltick')
const tickCallback = (payload) => {
const { cps, begin, end, cycle, time } = payload;
this.cps = cps;
this.cycle = cycle;
const currentTime = this.collator.calculateOffset(time) + time;
processHaps(begin, end, currentTime);
this.time_at_last_tick_message = currentTime;
};
const { cps, begin, end, cycle, time } = payload
this.cps = cps
this.cycle = cycle
const currentTime = this.collator.calculateOffset(time) + time
processHaps(begin, end, currentTime)
this.time_at_last_tick_message = currentTime
}
const processHaps = (begin, end, currentTime) => {
if (this.started === false) {
return;
return
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' })
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
const targetTime = timeUntilTrigger + currentTime + this.latency;
const duration = cycleToSeconds(hap.duration, this.cps);
onTrigger?.(hap, 0, duration, this.cps, targetTime);
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps)
const targetTime = timeUntilTrigger + currentTime + this.latency
const duration = cycleToSeconds(hap.duration, this.cps)
onTrigger?.(hap, 0, duration, this.cps, targetTime)
}
});
};
})
}
// receive messages from worker clock and process them
this.channel.onmessage = (message) => {
if (!this.started) {
return;
return
}
const { payload, type } = message.data;
const { payload, type } = message.data
switch (type) {
case 'tick': {
tickCallback(payload);
tickCallback(payload)
}
}
};
}
}
sendMessage(type, payload) {
this.worker.port.postMessage({ type, payload, id: this.id });
this.worker.port.postMessage({ type, payload, id: this.id })
}
now() {
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
return this.cycle + gap;
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps
return this.cycle + gap
}
setCps(cps = 1) {
this.sendMessage('cpschange', { cps });
this.sendMessage('cpschange', { cps })
}
setCycle(cycle) {
this.sendMessage('setcycle', { cycle });
this.sendMessage('setcycle', { cycle })
}
setStarted(started) {
this.sendMessage('toggle', { started });
this.started = started;
this.onToggle?.(started);
this.sendMessage('toggle', { started })
this.started = started
this.onToggle?.(started)
}
start() {
logger('[cyclist] start');
this.setStarted(true);
logger('[cyclist] start')
this.setStarted(true)
}
stop() {
logger('[cyclist] stop');
this.collator.reset();
this.setStarted(false);
logger('[cyclist] stop')
this.collator.reset()
this.setStarted(false)
}
setPattern(pat, autostart = false) {
this.pattern = pat;
this.pattern = pat
if (autostart && !this.started) {
this.start();
this.start()
}
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
const onsets = haps.filter((h) => h.hasOnset())
console.log(
`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,27 +4,27 @@ Copyright (C) 2024 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, reify, silence, register } from './pattern.mjs';
import { Pattern, reify, silence, register } from './pattern.mjs'
import { _mod, clamp, objectMap } from './util.mjs';
import { _mod, clamp, objectMap } from './util.mjs'
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
const array = Array.isArray(lookup)
const len = Object.keys(lookup).length
lookup = objectMap(lookup, reify);
lookup = objectMap(lookup, reify)
if (len === 0) {
return silence;
return silence
}
return pat.fmap((i) => {
let key = i;
let key = i
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1)
}
return lookup[key];
});
};
return lookup[key]
})
}
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
@ -44,14 +44,14 @@ const _pick = function (lookup, pat, modulo = true) {
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
;[pat, lookup] = [lookup, pat]
}
return __pick(lookup, pat);
};
return __pick(lookup, pat)
}
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
return _pick(lookup, pat, false).innerJoin()
})
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
@ -63,8 +63,8 @@ const __pick = register('pick', function (lookup, pat) {
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
return _pick(lookup, pat, true).innerJoin()
})
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
@ -78,8 +78,8 @@ export const pickmod = register('pickmod', function (lookup, pat) {
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
return pat.apply(pick(lookup, funcs))
})
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
@ -89,8 +89,8 @@ export const pickF = register('pickF', function (lookup, funcs, pat) {
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
return pat.apply(pickmod(lookup, funcs))
})
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
@ -98,8 +98,8 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
return _pick(lookup, pat, false).outerJoin()
})
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
@ -108,8 +108,8 @@ export const pickOut = register('pickOut', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
return _pick(lookup, pat, true).outerJoin()
})
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
@ -117,8 +117,8 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
return _pick(lookup, pat, false).restartJoin()
})
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
@ -134,8 +134,8 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
return _pick(lookup, pat, true).restartJoin()
})
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
@ -143,8 +143,8 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
return _pick(lookup, pat, false).resetJoin()
})
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
@ -153,8 +153,8 @@ export const pickReset = register('pickReset', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
return _pick(lookup, pat, true).resetJoin()
})
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
@ -170,9 +170,12 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
export const { inhabit, pickSqueeze } = register(
['inhabit', 'pickSqueeze'],
function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin()
}
)
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
@ -185,9 +188,12 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
export const { inhabitmod, pickmodSqueeze } = register(
['inhabitmod', 'pickmodSqueeze'],
function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin()
}
)
/**
* Pick from the list of values (or patterns of values) via the index using the given
@ -200,14 +206,14 @@ export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSq
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
xs = xs.map(reify)
if (xs.length == 0) {
return silence;
return silence
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
const key = _mod(Math.round(i), xs.length)
return xs[key]
})
.squeezeJoin();
};
.squeezeJoin()
}

View file

@ -1,10 +1,10 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { NeoCyclist } from './neocyclist.mjs'
import { Cyclist } from './cyclist.mjs'
import { evaluate as _evaluate } from './evaluate.mjs'
import { errorLogger, logger } from './logger.mjs'
import { setTime } from './time.mjs'
import { evalScope } from './evaluate.mjs'
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'
export function repl({
defaultOutput,
@ -33,70 +33,72 @@ export function repl({
widgets: [],
pending: false,
started: false,
};
}
const transpilerOptions = {
id,
};
}
const updateState = (update) => {
Object.assign(state, update);
state.isDirty = state.code !== state.activeCode;
state.error = state.evalError || state.schedulerError;
onUpdateState?.(state);
};
Object.assign(state, update)
state.isDirty = state.code !== state.activeCode
state.error = state.evalError || state.schedulerError
onUpdateState?.(state)
}
const schedulerOptions = {
onTrigger: getTrigger({ defaultOutput, getTime }),
getTime,
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
updateState({ started })
onToggle?.(started)
},
setInterval,
clearInterval,
beforeStart,
};
}
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
let pPatterns = {};
let anonymousIndex = 0;
let allTransform;
let eachTransform;
sync && typeof SharedWorker != 'undefined'
? new NeoCyclist(schedulerOptions)
: new Cyclist(schedulerOptions)
let pPatterns = {}
let anonymousIndex = 0
let allTransform
let eachTransform
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
eachTransform = undefined;
return silence;
};
pPatterns = {}
anonymousIndex = 0
allTransform = undefined
eachTransform = undefined
return silence
}
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
return pat.__pure;
return pat.__pure
}
return pat;
return pat
}
const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
return pattern;
};
setTime(() => scheduler.now()); // TODO: refactor?
pattern = editPattern?.(pattern) || pattern
await scheduler.setPattern(pattern, autostart)
return pattern
}
setTime(() => scheduler.now()) // TODO: refactor?
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const stop = () => scheduler.stop()
const start = () => scheduler.start()
const pause = () => scheduler.pause()
const toggle = () => scheduler.toggle()
const setCps = (cps) => {
scheduler.setCps(unpure(cps));
return silence;
};
scheduler.setCps(unpure(cps))
return silence
}
/**
* Changes the global tempo to the given cycles per minute
@ -109,9 +111,9 @@ export function repl({
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => {
scheduler.setCps(unpure(cpm) / 60);
return silence;
};
scheduler.setCps(unpure(cpm) / 60)
return silence
}
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@ -128,11 +130,11 @@ export function repl({
* all(x => x.pianoroll())
* ```
*/
let allTransforms = [];
let allTransforms = []
const all = function (transform) {
allTransforms.push(transform);
return silence;
};
allTransforms.push(transform)
return silence
}
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
* ```
* $: sound("bd - cp sd")
@ -141,50 +143,50 @@ export function repl({
* ```
*/
const each = function (transform) {
eachTransform = transform;
return silence;
};
eachTransform = transform
return silence
}
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) {
// allows muting a pattern x with x_ or _x
return silence;
return silence
}
if (id === '$') {
// allows adding anonymous patterns with $:
id = `$${anonymousIndex}`;
anonymousIndex++;
id = `$${anonymousIndex}`
anonymousIndex++
}
pPatterns[id] = this;
return this;
};
pPatterns[id] = this
return this
}
Pattern.prototype.q = function (id) {
return silence;
};
return silence
}
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
return this.p(i)
},
configurable: true,
});
})
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
return this.p(i)
},
configurable: true,
});
Pattern.prototype[`q${i}`] = silence;
})
Pattern.prototype[`q${i}`] = silence
}
} catch (err) {
console.warn('injectPatternMethods: error:', err);
console.warn('injectPatternMethods: error:', err)
}
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
return pat._fast(cpm / 60 / scheduler.cps)
})
return evalScope({
all,
each,
@ -194,50 +196,52 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
});
};
})
}
const evaluate = async (code, autostart = true, shouldHush = true) => {
if (!code) {
throw new Error('no code to evaluate');
throw new Error('no code to evaluate')
}
try {
updateState({ code, pending: true });
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code });
allTransforms = []; // reset all transforms
shouldHush && hush();
updateState({ code, pending: true })
await injectPatternMethods()
setTime(() => scheduler.now()) // TODO: refactor?
await beforeEval?.({ code })
allTransforms = [] // reset all transforms
shouldHush && hush()
if (mondo) {
code = `mondolang\`${code}\``;
code = `mondolang\`${code}\``
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions)
if (Object.keys(pPatterns).length) {
let patterns = [];
let patterns = []
for (const [key, value] of Object.entries(pPatterns)) {
patterns.push(value.withState((state) => state.setControls({ id: key })));
patterns.push(value.withState((state) => state.setControls({ id: key })))
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
patterns = patterns.map((x) => eachTransform(x))
}
pattern = stack(...patterns);
pattern = stack(...patterns)
} else if (eachTransform) {
pattern = eachTransform(pattern);
pattern = eachTransform(pattern)
}
if (allTransforms.length) {
for (let i in allTransforms) {
pattern = allTransforms[i](pattern);
pattern = allTransforms[i](pattern)
}
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
const message = `got "${typeof evaluated}" instead of pattern`
throw new Error(
message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')
)
}
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
logger(`[eval] code updated`)
pattern = await setPattern(pattern, autostart)
updateState({
miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [],
@ -246,18 +250,18 @@ export function repl({
evalError: undefined,
schedulerError: undefined,
pending: false,
});
afterEval?.({ code, pattern, meta });
return pattern;
})
afterEval?.({ code, pattern, meta })
return pattern
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false });
onEvalError?.(err);
logger(`[eval] error: ${err.message}`, 'error')
console.error(err)
updateState({ evalError: err, pending: false })
onEvalError?.(err)
}
};
const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
}
const setCode = (code) => updateState({ code })
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }
}
export const getTrigger =
@ -267,13 +271,13 @@ export const getTrigger =
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t);
await defaultOutput(hap, deadline, duration, cps, t)
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(hap, getTime(), cps, t);
await hap.context.onTrigger(hap, getTime(), cps, t)
}
} catch (err) {
errorLogger(err, 'getTrigger');
errorLogger(err, 'getTrigger')
}
};
}

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