diff --git a/packages/preview/typshade/0.1.3/LICENSE b/packages/preview/typshade/0.1.3/LICENSE new file mode 100644 index 0000000000..a66a55f980 --- /dev/null +++ b/packages/preview/typshade/0.1.3/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/packages/preview/typshade/0.1.3/README.md b/packages/preview/typshade/0.1.3/README.md new file mode 100644 index 0000000000..964f3ddc08 --- /dev/null +++ b/packages/preview/typshade/0.1.3/README.md @@ -0,0 +1,206 @@ +# Typshade + +![Typshade alignment overview](images/readme-overview.png) + +Typshade is a Typst package for visualizing multiple sequence alignments in bioinformatics. + +It provides a Typst-native interface centered on `shade(...)`, offering a readable and composable way to render alignments, add annotations, and incorporate logos, structure tracks, and graph tracks. + +Inspired by [TeXshade](https://ctan.org/pkg/texshade), Typshade rethinks alignment visualization with a focus on clarity, composability, and a Typst-native user experience. + +## Why Typshade? + +TeXshade is powerful, but its UI reflects TeX: many global commands, implicit state, and order-sensitive setup before the alignment is rendered. Typshade keeps the feature set while making the source read like a figure specification. + +TeXshade style: + +```latex +\begin{texshade}{alignment.msf} + \shadingmode[similar]{identical} + \shadingcolors{blues} + \residuesperline{45} + \setends{1}{80..125} + \showruler{top}{1} + \rulersteps{10} + \showconsensus{bottom} + \showsequencelogo{top} + \shaderegion{1}{NPA}{White}{BrickRed} + \feature{top}{1}{NXX[ST]N}{box[Yellow]}{motif} +\end{texshade} +``` + +Typshade style: + +```typst +#let alignment = read("alignment.msf", encoding: none) + +#shade( + alignment, + format: "msf", + figure: publication( + similarity: "blues", + region: "80..125", + logo: "charge", + motifs: ( + "NPA": (bg: "BrickRed", text: "active site"), + "NXX[ST]N": "motif", + ), + ), +) +``` + +The lower-level helpers are still available through `commands:` when you need precise control, but new documents can usually start from the kind of figure you want: publication figure, motif map, structure map, or logo analysis. + +## Quick Start + +```typst +#import "@preview/typshade:0.1.3": * + +#let alignment = read("alignment.msf", encoding: none) + +#shade( + alignment, + format: "msf", + theme: "screen", + figure: motif-map(auto), +) +``` + +`read(..., encoding: none)` remains supported on Typst 0.15 and later. On Typst 0.15 or later, you can additionally pass a resolved project path and let Typshade read the source inside the package: + +```typst +#shade(path("alignment.msf"), format: "msf", figure: motif-map(auto)) +``` + +## Preview + +Protein alignment with similarity shading, motif annotations, a ruler, +a conservation track, and a legend: + +![Protein alignment preview](images/readme-preview-1.png) + +Protein alignment with hydropathy-based functional coloring: + +![Hydropathy preview](images/readme-preview-2.png) + +Nucleotide alignment with DNA coloring, a sequence logo, a conservation track, +and a ruler: + +![DNA alignment preview](images/readme-preview-3.png) + +## Typst-Native Helpers + +- `shade(...)`: Named-option alignment renderer for new documents. +- `figure:`: Purpose-level recipe slot for complete figure designs. +- `publication`, `motif-map`, `structure-map`, `logo-analysis`, `overview`: High-level recipes. +- `similar`, `identical`, `diverse`, `functional`, `lines`, `window`, `ruler`, `consensus`, `logo`, `legend`: Compact command helpers; `lines(auto)` and `fit: "container"` fit the current Typst container. +- `auto-layout(...)`, `auto-page(...)`, and `fit: "page"`: Typst-aware line length and page-aware block splitting for long figures. +- `color-scheme`, `scoring-mode`, `sequence-window`: Small, readable option helpers. +- `ruler-track`, `consensus-track`, `sequence-logo`, `subfamily-logo`, `legend-track`: Track helpers. +- `structure-tracks(...)`: Adds topology/secondary-structure tracks from sidecar files. +- `shade-preset("publication" | "overview" | "logo" | "functional" | "structure")`: Reusable command bundles. +- `shade-theme("classic" | "print" | "screen" | "warm" | "nature")` and `visual-theme(...)`: Color/style bundles. +- `highlight`, `tint`, `emphasize`, `mark`, `motif`, `graph`: Readable command builders for common annotations; graph metrics include conservation, entropy, gap-fraction, coverage, identity, hydrophobicity, molecular weight, and charge. +- `select-range`, `select-motif`, `select-metric`, `select-and`, `select-or`, `select-not`, and `select-pad`: Composable Selection DSL values for windows, highlights, marks, graphs, and analysis helpers. +- `cell-style(ctx => ...)`: Data-driven per-cell styling with Typst functions. +- `pdb-point`, `pdb-line`, `pdb-plane`: Safer constructors for PDB selections. +- `alignment-position("left" | "center" | "right")`: Overrides the default left-aligned block placement. +- `alignment-summary(...)`, `alignment-debug(...)`, `cell-inspect(...)`, and `selection-preview(...)`: In-document inspection helpers. +- `sequence-list(...)` and `selection-table(...)`: Typst tables for data-aware reports. +- `percent-identity(...)`, `percent-similarity(...)`, and `similarity-table(...)`: Pairwise identity/similarity analysis. +- `alignment-data(...)` and `parse-alignment(...)`: Data access helpers for custom Typst logic. + +## TeXshade To Typshade + +| TeXshade idea | Typshade API | +|---|---| +| `texshade` environment | `shade(read("alignment.msf", encoding: none), format: "msf", figure: publication(...))`, or `shade(path("alignment.msf"), format: "msf", ...)` on Typst 0.15+ | +| `shadingmode`, `shadingcolors`, `threshold` | `similar`, `identical`, `diverse`, `functional`, or `scoring-mode`, `color-scheme`, `threshold` | +| `residuesperline`, `setends` | `lines`, `window`, `fit`, `auto-layout`, `auto-page`, or Selection DSL values with `sequence-window` | +| `shownames`, `shownumbering`, `showconsensus`, `showruler` | `names`, `numbers`, `consensus`, `ruler`, or the fine-grained track helpers | +| `showsequencelogo`, `showsubfamilylogo`, `showlegend` | `logo`, `subfamily-logo`, `legend` | +| `shaderegion`, `tintregion`, `emphregion`, `feature` | `highlight`, `tint`, `emphasize`, `mark`, `motif`, `graph` | +| `includeDSSP`, `includeSTRIDE`, `includeHMMTOP`, `includePHD*` | `structure-tracks`, `dssp-track`, `stride-track`, `hmmtop-track`, `phd-topology-track`, `phd-secondary-track` | +| font and spacing macros | `text-family`, `text-weight`, `text-posture`, `text-size`, `block-gap`, `feature-slot-space` | + +See the [full documentation](https://github.com/rice8y/typshade/blob/v0.1.3/docs/documentation.pdf) for the full guide and a larger correspondence table. + +## Smart Recipes + +Recipes inspect the alignment before rendering. For example, `motif-map(auto)` detects common motifs for the sequence type, focuses the region around them, chooses a readable line length, adds conservation when useful, and enables a logo only when the figure stays readable. Override any option when you need exact control. + +## Fine-Grained Control + +Macro-style command names are intentionally not part of the public API. Use Typst-shaped command helpers when you need detailed control: + +- Scoring: `threshold`, `weight-table`, `set-weight`, `gap-penalty`, `residue-style`, `functional-group`. +- Tracks: `names-track`, `numbering-track`, `consensus-name`, `consensus-symbols`, `ruler-name`, `ruler-marker`, `logo-color`. +- Sequence layout: `start-number`, `sequence-length`, `domain`, `hide-sequence`, `sequence-order`, `separation-line`. +- Features and structure: `feature-rule`, `feature-text-label`, `backtranslation-label`, `show-structure-types`, `structure-appearance`, `stride-track`, `dssp-track`, `hmmtop-track`, `phd-topology-track`, `phd-secondary-track`. +- Typography and spacing: `text-family`, `text-weight`, `text-posture`, `text-size`, `character-stretch`, `line-stretch`, `block-gap`, `feature-slot-space`. + +## Custom Control + +Use recipes when you know the purpose of the figure: + +```typst +#let alignment = read("alignment.msf", encoding: none) + +#shade( + alignment, + format: "msf", + figure: publication( + region: "80..125", + logo: "charge", + motifs: ( + "NPA": (bg: "BrickRed", text: "active site"), + "NXX[ST]N": "glycosylation", + ), + ), +) +``` + +Use `commands:` when you want to assemble visible parts yourself: + +```typst +#let alignment = read("alignment.msf", encoding: none) + +#shade( + alignment, + format: "msf", + preset: "publication", + theme: "screen", + commands: ( + similar(colors: "blues", threshold: 45), + lines(45), + window(1, "80..125"), + ruler("top", sequence: 1, every: 10), + consensus("bottom", name: "conservation"), + logo("top", colors: "charge"), + legend(), + highlight(1, "NPA", bg: "BrickRed"), + motif(1, "NXX[ST]N", text: "motif"), + graph("bottom", 1, "all", "conservation", kind: "color", options: ("ColdHot",)), + ), +) +``` + +You can also mix recipe output with explicit, reproducible helper lists: + +```typst +#let alignment = read("alignment.msf", encoding: none) + +#shade( + alignment, + format: "msf", + figure: publication(region: "80..125"), + commands: ( + highlight(1, "NXX[DE][KR]XXQ", fg: "White", bg: "BrickRed"), + sequence-logo(position: "top"), + ), +) +``` + +# License + +This project is distributed under the GPL v2 License. See [LICENSE](LICENSE) for details. diff --git a/packages/preview/typshade/0.1.3/images/readme-overview.png b/packages/preview/typshade/0.1.3/images/readme-overview.png new file mode 100644 index 0000000000..d6061b9d0e Binary files /dev/null and b/packages/preview/typshade/0.1.3/images/readme-overview.png differ diff --git a/packages/preview/typshade/0.1.3/images/readme-preview-1.png b/packages/preview/typshade/0.1.3/images/readme-preview-1.png new file mode 100644 index 0000000000..254c23ae0f Binary files /dev/null and b/packages/preview/typshade/0.1.3/images/readme-preview-1.png differ diff --git a/packages/preview/typshade/0.1.3/images/readme-preview-2.png b/packages/preview/typshade/0.1.3/images/readme-preview-2.png new file mode 100644 index 0000000000..f1811b13e1 Binary files /dev/null and b/packages/preview/typshade/0.1.3/images/readme-preview-2.png differ diff --git a/packages/preview/typshade/0.1.3/images/readme-preview-3.png b/packages/preview/typshade/0.1.3/images/readme-preview-3.png new file mode 100644 index 0000000000..ece3ee65a6 Binary files /dev/null and b/packages/preview/typshade/0.1.3/images/readme-preview-3.png differ diff --git a/packages/preview/typshade/0.1.3/internal/engine/commands.typ b/packages/preview/typshade/0.1.3/internal/engine/commands.typ new file mode 100644 index 0000000000..8032f56ee2 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/engine/commands.typ @@ -0,0 +1,24 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#let _add-command(out, value) = { + if value == none { + return out + } + if type(value) == array { + for item in value { + out = _add-command(out, item) + } + } else { + out.push(value) + } + out +} + +#let command-pack(..items) = { + let out = () + for item in items.pos() { + out = _add-command(out, item) + } + out +} diff --git a/packages/preview/typshade/0.1.3/internal/engine/config.typ b/packages/preview/typshade/0.1.3/internal/engine/config.typ new file mode 100644 index 0000000000..314a1e6903 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/engine/config.typ @@ -0,0 +1,1045 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../model/parser.typ": _chars, _lower, _upper, read-dssp, read-hmmtop, read-phd-secondary, read-phd-topology, read-stride, read-tcoffee +#import "../model/palette.typ": _dna-groups, _dna-sims, _functional-presets, _pep-groups, _pep-sims, resolve-color +#import "../model/pdb.typ": pdb-selection-list +#import "../model/text-style.typ": _default-text-styles, _set-text-style + +#let _default-style(kind) = if kind == "identical" { + ( + nomatch: (fg: "Black", bg: "White", case: "upper"), + similar: (fg: "Black", bg: "Magenta", case: "upper"), + conserved: (fg: "White", bg: "RoyalBlue", case: "upper"), + allmatch: (fg: "Goldenrod", bg: "RoyalPurple", case: "upper"), + ) +} else { ( + nomatch: (fg: "Black", bg: "White", case: "upper"), + similar: (fg: "Black", bg: "Magenta", case: "upper"), + conserved: (fg: "White", bg: "RoyalBlue", case: "upper"), + allmatch: (fg: "Goldenrod", bg: "RoyalPurple", case: "upper"), +) } + +#let _apply-shading-colors(config, name) = { + let style = config.at("residue-style") + if name == "blues" { + style.insert("similar", (fg: "Black", bg: "Magenta", case: "upper")) + style.insert("conserved", (fg: "White", bg: "RoyalBlue", case: "upper")) + style.insert("allmatch", (fg: "Goldenrod", bg: "RoyalPurple", case: "upper")) + } else if name == "greens" { + style.insert("similar", (fg: "Black", bg: "GreenYellow", case: "upper")) + style.insert("conserved", (fg: "White", bg: "PineGreen", case: "upper")) + style.insert("allmatch", (fg: "YellowOrange", bg: "OliveGreen", case: "upper")) + } else if name == "reds" { + style.insert("similar", (fg: "Black", bg: "YellowOrange", case: "upper")) + style.insert("conserved", (fg: "White", bg: "BrickRed", case: "upper")) + style.insert("allmatch", (fg: "YellowGreen", bg: "Mahagony", case: "upper")) + } else if name == "grays" { + style.insert("similar", (fg: "Black", bg: "LightGray", case: "upper")) + style.insert("conserved", (fg: "White", bg: "DarkGray", case: "upper")) + style.insert("allmatch", (fg: "White", bg: "Black", case: "upper")) + } else if name == "black" { + style.insert("similar", (fg: "Black", bg: "White", case: "upper")) + style.insert("conserved", (fg: "White", bg: "Black", case: "upper")) + style.insert("allmatch", (fg: "White", bg: "Black", case: "upper")) + } else if config.at("shading-schemes").keys().contains(name) { + let saved = config.at("shading-schemes").at(name) + for key in saved.at("residue-style").keys() { + style.insert(key, saved.at("residue-style").at(key)) + } + for key in saved.at("gaps").keys() { + config.at("gaps").insert(key, saved.at("gaps").at(key)) + } + } +} + +#let _style-record(fg, bg, case, style) = (fg: fg, bg: bg, case: case, style: style) +#let _func-style-record(name, residues, fg, bg, case, style) = (name: name, residues: _upper(str(residues)), fg: fg, bg: bg, case: case, style: style) + +#let _residue-masses = ( + A: 89.1, C: 121.2, D: 133.1, E: 147.1, F: 165.2, G: 75.1, H: 155.2, I: 131.2, + K: 146.2, L: 131.2, M: 149.2, N: 132.1, P: 115.1, Q: 146.1, R: 174.2, S: 105.1, + T: 119.1, V: 117.1, W: 204.2, Y: 181.2, +) + +#let _peptide-charges = ( + C: -0.03, D: -1.0, E: -1.0, H: 0.165, K: 1.0, R: 1.0, +) + +#let _standard-genetic-code = ( + A: "GCN", C: "TGY", D: "GAY", E: "GAR", F: "TTY", G: "GGN", H: "CAY", I: "ATH", + K: "AAR", L: "YTN", M: "ATG", N: "AAY", P: "CCN", Q: "CAR", R: "MGN", S: "WSN", + T: "ACN", V: "GTN", W: "TGG", Y: "TAY", ".": "TRR", +) + +#let _clean-residue-string(sequence) = _upper(str(sequence)).replace(".", "").replace("-", "").replace(" ", "").replace("\n", "") + +#let molweight(sequence, unit: "Da") = { + let total = 0.0 + for residue in _chars(_clean-residue-string(sequence)) { + total += _residue-masses.at(residue, default: 0.0) + } + if unit == "kDa" or unit == "kda" { + str(calc.round(total / 10.0) / 100.0) + } else { + str(calc.round(total)) + } +} + +#let charge(sequence, termini: "o") = { + let total = 0.0 + for residue in _chars(_clean-residue-string(sequence)) { + total += _peptide-charges.at(residue, default: 0.0) + } + let mode = _lower(str(termini)) + if mode == "o" or mode == "n" { + total += 0.91 + } + if mode == "o" or mode == "c" { + total -= 1.0 + } + str(calc.round(total * 100.0) / 100.0) +} + +#let _style-snapshot(config) = { + let styles = (:) + for key in config.at("residue-style").keys() { + let value = config.at("residue-style").at(key) + styles.insert(key, ( + fg: value.at("fg"), + bg: value.at("bg"), + case: value.at("case", default: "upper"), + style: value.at("style", default: "normal"), + )) + } + let gaps = (:) + for key in config.at("gaps").keys() { + gaps.insert(key, config.at("gaps").at(key)) + } + (residue-style: styles, gaps: gaps) +} + +#let _group-list(value) = { + if type(value) == str { + return value.split(",").map(group => _upper(group.trim())).filter(group => group != "") + } + value.map(group => _upper(str(group).trim())).filter(group => group != "") +} + +#let _ref-list(value) = { + if type(value) == str { + let out = () + for item in value.split(",") { + let trimmed = item.trim() + if trimmed == "" { + continue + } + let range-hit = trimmed.matches(regex("^(\\d+)\\-(\\d+)$")) + if range-hit.len() > 0 { + let start = int(range-hit.first().captures.at(0)) + let stop = int(range-hit.first().captures.at(1)) + let step = if start <= stop { 1 } else { -1 } + let current = start + while current != stop + step { + out.push(current) + current += step + } + } else { + out.push(trimmed) + } + } + return out + } + if type(value) == array { + return value + } + (value,) +} + +#let _type-list(value) = if type(value) == str { value.split(",").map(v => v.trim()).filter(v => v != "") } else { value } + +#let _hide-structure-types(config, filetype, types) = { + let remove = _type-list(types) + let kept = () + for item in config.at("structure-show").at(filetype, default: ()) { + if not remove.contains(item) { + kept.push(item) + } + } + config.at("structure-show").insert(filetype, kept) +} + +#let _default-config() = { + let config = ( + threshold: 50, + allmatch-threshold: 100, + weight-table: "identity", + custom-weights: (:), + gap-penalty: 0, + residues-per-line: 60, + auto-layout: ( + fit: none, + min: 1, + max: none, + ), + auto-page: ( + enabled: false, + blocks: none, + repeat-legend: false, + ), + font: "DejaVu Sans Mono", + font-families: ( + serif: "Times New Roman", + sans: "Arial", + ), + font-size: 9pt, + line-gap: 2pt, + block-gap: 8pt, + fixed-block-space: false, + char-stretch: 1.0, + numbering-width-digits: 4, + text-styles: _default-text-styles, + alignment: "left", + align-right-labels: false, + seq-type: none, + show-leading-gaps: true, + single-seq-shift: none, + keep-single-seq-gaps: false, + hide-residues: false, + hide-allmatch-positions: false, + hide-seqs: false, + no-shade: (), + sequence-names: (:), + hidden-names: (), + hidden-numbers: (), + sequence-name-colors: (:), + sequence-number-colors: (:), + names-color: "Black", + numbering-color: "Black", + start-numbers: (:), + sequence-lengths: (:), + allow-zero: false, + stop-char: "*", + names: ("show": true, "position": "left"), + numbering: ("show": true, "left": false, "right": true), + consensus: ( + "show": true, + "position": "bottom", + "scale": none, + "name": "consensus", + "source": "all", + "symbols": ("none": "", "conserved": "*", "allmatch": "!"), + "colors": ( + "none": (fg: "Black", bg: "White"), + "conserved": (fg: "Black", bg: "White"), + "allmatch": (fg: "Black", bg: "White"), + ), + ), + ruler: ("show": false, "position": "top", "sequence": 1, "steps": 10, "color": "Black"), + ruler-colors: ("top": "Black", "bottom": "Black"), + ruler-names: ("top": "", "bottom": ""), + ruler-name-colors: ("top": "Black", "bottom": "Black"), + ruler-labels: ("top": (:), "bottom": (:)), + ruler-spacing: ("top": 0pt, "bottom": 0pt), + ruler-rotation: ("top": false, "bottom": false), + gaps: ("char": ".", "fg": "Black", "bg": "White", "rule-thickness": 0.3pt), + shading: ("mode": "identical", "option": none, "scheme": "blues", "reference": none), + shading-schemes: (:), + tcoffee: ("source": none, "scores": (:)), + pep-groups: _pep-groups, + dna-groups: _dna-groups, + pep-sims: _pep-sims, + dna-sims: _dna-sims, + functional-groups: _functional-presets, + functional-style-overrides: (:), + functional-default: "charge", + residue-style: _default-style("identical"), + cell-styles: (), + sequence-logo: ("show": false, "position": "top", "colorset": none, "name": "logo", "scale": "leftright", "stretch": 1), + subfamily-logo: ("show": false, "position": "top", "colorset": none, "name": "subfamily", "negative-name": "remaining", "show-negatives": true), + logo-scale: ("show": true, "position": "leftright", "color": "Black"), + logo-colors: ("default": none, "map": (:)), + relevance: ("show": true, "color": "Black", "char": "*", "threshold": 0.1), + frequency-correction: false, + legend: ("show": false, "color": "Black", "dx": 0pt, "dy": 0pt), + captions: ("top": none, "bottom": none, "short": none), + separation-lines: (), + separation-space: 6pt, + bar-graph-stretch: ("default": 1.0), + color-scale-stretch: ("default": 1.0), + feature-style-names: (:), + feature-text-names: (:), + feature-style-name-colors: ("default": "Black"), + feature-text-name-colors: ("default": "Black"), + feature-slot-spacing: ( + ttttop: 0pt, + tttop: 0pt, + ttop: 0pt, + top: 0pt, + bottom: 0pt, + bbottom: 0pt, + bbbottom: 0pt, + bbbbottom: 0pt, + ), + subfamily: (), + structure-show: ( + "DSSP": ("alpha", "3-10", "pi", "beta"), + "STRIDE": ("alpha", "3-10", "pi", "beta"), + "PHDsec": ("alpha", "beta"), + "PHDtopo": ("internal", "external", "TM"), + "HMMTOP": ("TM"), + ), + structure-appearance: ( + "PHDtopo:internal": ("position": "bottom", "style": "-", "text": "int."), + "PHDtopo:external": ("position": "top", "style": ",-,", "text": "ext."), + "PHDtopo:TM": ("position": "top", "style": "box", "text": "TM"), + "HMMTOP:internal": ("position": "bottom", "style": "-", "text": "int."), + "HMMTOP:external": ("position": "top", "style": ",-,", "text": "ext."), + "HMMTOP:TM": ("position": "top", "style": "helix", "text": "TM"), + "PHDsec:alpha": ("position": "top", "style": "box", "text": "α\\numcount"), + "PHDsec:beta": ("position": "top", "style": "-->", "text": "β\\numcount"), + "STRIDE:alpha": ("position": "top", "style": "box:$\\alpha$\\numcount", "text": ""), + "STRIDE:3-10": ("position": "top", "style": "fill:$\\circ$", "text": "3-10"), + "STRIDE:pi": ("position": "top", "style": "---", "text": "π"), + "STRIDE:beta": ("position": "top", "style": "-->", "text": "β\\numcount"), + "STRIDE:bridge": ("position": "top", "style": "fill:$\\uparrow$", "text": ""), + "STRIDE:turn": ("position": "top", "style": ",-,", "text": "turn"), + "DSSP:alpha": ("position": "top", "style": "box:$\\alpha$\\numcount", "text": ""), + "DSSP:3-10": ("position": "top", "style": "fill:$\\circ$", "text": "3-10"), + "DSSP:pi": ("position": "top", "style": "---", "text": "π"), + "DSSP:beta": ("position": "top", "style": "-->", "text": "β\\numcount"), + "DSSP:bridge": ("position": "top", "style": "fill:$\\uparrow$", "text": ""), + "DSSP:turn": ("position": "top", "style": ",-,", "text": "turn"), + "DSSP:bend": ("position": "top", "style": "fill:$\\diamond$", "text": ""), + ), + dssp-second-column: true, + structure-data: (), + sequence-windows: (), + emph-default: "italic", + tint-default: "normal", + shade-regions: (), + tint-regions: (), + lower-regions: (), + emph-regions: (), + frame-regions: (), + hidden: (), + killed: (), + order: none, + features: (), + feature-rule: 0.5pt, + genetic-code: _standard-genetic-code, + backtranslation: ( + label-style: "alternating", + label-size: "tiny", + text-style: "horizontal", + text-size: "tiny", + ), + exported-consensus: none, + ) + _apply-shading-colors(config, "blues") + config +} + +#let _command(kind, fields) = { + let out = fields + out.insert("kind", kind) + out +} + +#let sequence-type(value) = _command("sequence-type", (value: value)) +#let scoring-mode(mode, option: none) = _command("scoring-mode", (mode: mode, option: option)) +#let color-scheme(name) = _command("color-scheme", (name: name)) +#let define-color-scheme(name) = _command("define-color-scheme", (name: name)) +#let threshold(value) = _command("threshold", (value: value)) +#let shade-all-residues() = _command("shade-all-residues", (:)) +#let all-match-threshold(value: 100) = _command("all-match-threshold", (value: value)) +#let disable-all-match-threshold() = _command("disable-all-match-threshold", (:)) +#let hide-all-match-positions() = _command("hide-all-match-positions", (:)) +#let show-all-match-positions() = _command("show-all-match-positions", (:)) +#let weight-table(name) = _command("weight-table", (name: name)) +#let set-weight(residue-a, residue-b, value) = _command("set-weight", (residue-a: residue-a, residue-b: residue-b, value: value)) +#let gap-penalty(value) = _command("gap-penalty", (value: value)) +#let residues-per-line(value) = _command("residues-per-line", (value: value)) +#let auto-layout(fit: "container", min: 1, max: none) = _command("auto-layout", (fit: fit, min: min, max: max)) +#let auto-page(blocks: auto, repeat-legend: true) = _command("auto-page", (blocks: blocks, repeat-legend: repeat-legend)) +#let numbering-track(position: "right", color: none) = _command("numbering-track", (position: position, color: color)) +#let no-numbering-track() = _command("no-numbering-track", (:)) +#let names-track(position: "left", color: none) = _command("names-track", (position: position, color: color)) +#let no-names-track() = _command("no-names-track", (:)) +#let sequence-name(sequence, name) = _command("sequence-name", (sequence: sequence, name: name)) +#let names-color(color) = _command("names-color", (color: color)) +#let sequence-name-color(sequences, color) = _command("sequence-name-color", (sequences: sequences, color: color)) +#let hide-sequence-name(sequences) = _command("hide-sequence-name", (sequences: sequences)) +#let numbering-color(color) = _command("numbering-color", (color: color)) +#let sequence-number-color(sequences, color) = _command("sequence-number-color", (sequences: sequences, color: color)) +#let hide-sequence-number(sequences) = _command("hide-sequence-number", (sequences: sequences)) +#let consensus-track(position: "bottom", scale: none, name: none) = _command("consensus-track", (position: position, scale: scale, name: name)) +#let no-consensus-track() = _command("no-consensus-track", (:)) +#let consensus-name(name) = _command("consensus-name", (name: name)) +#let language(name) = _command("language", (name: name)) +#let consensus-symbols(none-symbol, conserved-symbol, allmatch-symbol) = _command("consensus-symbols", ("none": none-symbol, conserved: conserved-symbol, allmatch: allmatch-symbol)) +#let consensus-colors(none-fg: "Black", none-bg: "White", conserved-fg: "Black", conserved-bg: "White", allmatch-fg: "Black", allmatch-bg: "White") = _command("consensus-colors", (none-fg: none-fg, none-bg: none-bg, conserved-fg: conserved-fg, conserved-bg: conserved-bg, allmatch-fg: allmatch-fg, allmatch-bg: allmatch-bg)) +#let residue-style(target, fg, bg, case: "upper", style: "normal") = _command("residue-style", (target: target, fg: fg, bg: bg, case: case, style: style)) +#let cell-style(callback) = _command("cell-style", (callback: callback)) +#let clear-functional-groups() = _command("clear-functional-groups", (:)) +#let functional-group(name, residues, fg, bg, case: "upper", style: "normal") = _command("functional-group", (name: name, residues: residues, fg: fg, bg: bg, case: case, style: style)) +#let functional-style(residue, fg, bg, case: "upper", style: "normal") = _command("functional-style", (residue: residue, fg: fg, bg: bg, case: case, style: style)) +#let ruler-track(position: "top", sequence: 1, steps: none, color: none) = _command("ruler-track", (position: position, sequence: sequence, steps: steps, color: color)) +#let no-ruler-track(position: none) = _command("no-ruler-track", (position: position)) +#let ruler-steps(value, position: none) = _command("ruler-steps", (value: value, position: position)) +#let ruler-color(color, position: none) = _command("ruler-color", (color: color, position: position)) +#let ruler-name(name, position: none) = _command("ruler-name", (name: name, position: position)) +#let ruler-name-color(color, position: none) = _command("ruler-name-color", (color: color, position: position)) +#let ruler-marker(number, text, position: none, color: none) = _command("ruler-marker", (number: number, text: text, position: position, color: color)) +#let ruler-space(value, position: none) = _command("ruler-space", (value: value, position: position)) +#let rotate-ruler(..args) = { + let positional = args.pos() + let position = if positional.len() > 0 { positional.first() } else { args.named().at("position", default: none) } + _command("rotate-ruler", (position: position)) +} +#let unrotate-ruler(..args) = { + let positional = args.pos() + let position = if positional.len() > 0 { positional.first() } else { args.named().at("position", default: none) } + _command("unrotate-ruler", (position: position)) +} +#let gap-char(symbol) = _command("gap-char", (symbol: symbol)) +#let gap-rule(thickness) = _command("gap-rule", (thickness: thickness)) +#let gap-colors(foreground, background) = _command("gap-colors", (foreground: foreground, background: background)) +#let stop-char(symbol) = _command("stop-char", (symbol: symbol)) +#let peptide-groups(groups) = _command("peptide-groups", (groups: groups)) +#let dna-groups(groups) = _command("dna-groups", (groups: groups)) +#let peptide-similarities(residue, similars) = _command("peptide-similarities", (residue: residue, similars: similars)) +#let dna-similarities(residue, similars) = _command("dna-similarities", (residue: residue, similars: similars)) +#let start-number(sequence, start, selection: none) = _command("start-number", (sequence: sequence, start: start, selection: selection)) +#let allow-zero-numbering() = _command("allow-zero-numbering", (:)) +#let disallow-zero-numbering() = _command("disallow-zero-numbering", (:)) +#let sequence-length(sequence, length) = _command("sequence-length", (sequence: sequence, length: length)) +#let sequence-window(sequence, selection, start: none) = _command("sequence-window", (sequence: sequence, selection: selection, start: start)) +#let domain(sequence, selection) = sequence-window(sequence, selection) +#let domain-gap-rule(thickness) = gap-rule(thickness) +#let domain-gap-colors(foreground, background) = gap-colors(foreground, background) +#let region-highlight(sequence, selection, ..args) = { + let positional = args.pos() + let named = args.named() + let bg = if positional.len() >= 2 { positional.at(1) } else { named.at("bg", default: none) } + let fg-or-scheme = if positional.len() >= 1 { positional.at(0) } else { named.at("fg-or-scheme", default: none) } + let scheme = named.at("scheme", default: none) + let resolved-scheme = if scheme != none { scheme } else if bg == none { fg-or-scheme } else { none } + let fg = if bg == none { named.at("fg", default: none) } else { fg-or-scheme } + _command("region-highlight", (sequence: sequence, selection: selection, scheme: resolved-scheme, fg: fg, bg: bg, all: named.at("apply-to-all", default: false))) +} +#let highlight-block(sequence, selection, ..args) = { + let command = region-highlight(sequence, selection, ..args) + command.insert("all", args.named().at("apply-to-all", default: true)) + command +} +#let region-color-scheme(sequence, selection, scheme) = region-highlight(sequence, selection, scheme: scheme, apply-to-all: true) +#let region-tint(sequence, selection, intensity: "medium") = _command("region-tint", (sequence: sequence, selection: selection, intensity: intensity)) +#let tint-block(sequence, selection, intensity: "medium") = region-tint(sequence, selection, intensity: intensity) +#let tint-default(effect) = _command("tint-default", (effect: effect)) +#let region-lower(sequence, selection) = _command("region-lower", (sequence: sequence, selection: selection)) +#let lower-block(sequence, selection) = region-lower(sequence, selection) +#let region-emphasis(sequence, selection, style: "italic") = _command("region-emphasis", (sequence: sequence, selection: selection, style: style)) +#let emphasis-block(sequence, selection, style: "italic") = region-emphasis(sequence, selection, style: style) +#let emphasis-default(style) = _command("emphasis-default", (style: style)) +#let frame-block(sequence, selection, color: "Red") = _command("frame-block", (sequence: sequence, selection: selection, color: color)) +#let hide-sequence(sequence) = _command("hide-sequence", (sequence: sequence)) +#let hide-all-sequences() = _command("hide-all-sequences", (:)) +#let show-all-sequences() = _command("show-all-sequences", (:)) +#let remove-sequence(sequence) = _command("remove-sequence", (sequence: sequence)) +#let no-shade(sequences) = _command("no-shade", (sequences: sequences)) +#let separation-line(sequence) = _command("separation-line", (sequence: sequence)) +#let sequence-order(order) = _command("sequence-order", (order: order)) +#let feature(position, sequence, selection, style: "", text: "") = _command("feature", (position: position, sequence: sequence, selection: selection, style: style, text: text)) +#let feature-rule(thickness) = _command("feature-rule", (thickness: thickness)) +#let codon(residue, triplets) = _command("codon", (residue: residue, triplets: triplets)) +#let genetic-code(name) = _command("genetic-code", (name: name)) +#let backtranslation-label(..args) = { + let positional = args.pos() + let style = if positional.len() > 0 { positional.last() } else { args.named().at("style", default: "alternating") } + let size = args.named().at("size", default: "tiny") + _command("backtranslation-label", (style: style, size: size)) +} +#let backtranslation-text(..args) = { + let positional = args.pos() + let style = if positional.len() > 0 { positional.last() } else { args.named().at("style", default: "horizontal") } + let size = args.named().at("size", default: "tiny") + _command("backtranslation-text", (style: style, size: size)) +} +#let feature-text-label(position, name) = _command("feature-text-label", (position: position, name: name)) +#let feature-style-label(position, name) = _command("feature-style-label", (position: position, name: name)) +#let hide-feature-text-label(position) = _command("hide-feature-text-label", (position: position)) +#let hide-feature-style-label(position) = _command("hide-feature-style-label", (position: position)) +#let hide-feature-text-labels() = _command("hide-feature-text-labels", (:)) +#let hide-feature-style-labels() = _command("hide-feature-style-labels", (:)) +#let feature-text-label-color(color) = _command("feature-text-label-color", (color: color)) +#let feature-style-label-color(color) = _command("feature-style-label-color", (color: color)) +#let feature-text-label-color-at(position, color) = _command("feature-text-label-color-at", (position: position, color: color)) +#let feature-style-label-color-at(position, color) = _command("feature-style-label-color-at", (position: position, color: color)) +#let tcoffee-scores(source) = _command("tcoffee-scores", (source: source)) +#let sequence-logo-track(position: "top", colorset: none) = _command("sequence-logo-track", (position: position, colorset: colorset)) +#let no-sequence-logo-track() = _command("no-sequence-logo-track", (:)) +#let frequency-correction() = _command("frequency-correction", (:)) +#let no-frequency-correction() = _command("no-frequency-correction", (:)) +#let subfamily(sequences) = _command("subfamily", (sequences: sequences)) +#let subfamily-logo-track(position: "top", colorset: none) = _command("subfamily-logo-track", (position: position, colorset: colorset)) +#let no-subfamily-logo-track() = _command("no-subfamily-logo-track", (:)) +#let sequence-logo-name(name) = _command("sequence-logo-name", (name: name)) +#let subfamily-logo-name(name, negative-name: none) = _command("subfamily-logo-name", (name: name, negative-name: negative-name)) +#let logo-scale(position: "leftright", color: "Black") = _command("logo-scale", (position: position, color: color)) +#let no-logo-scale() = _command("no-logo-scale", (:)) +#let logo-stretch(value) = _command("logo-stretch", (value: value)) +#let negative-logo-values() = _command("negative-logo-values", (:)) +#let no-negative-logo-values() = _command("no-negative-logo-values", (:)) +#let relevance-threshold(value) = _command("relevance-threshold", (value: value)) +#let relevance-marker(char: "*", color: "Black") = _command("relevance-marker", (char: char, color: color)) +#let no-relevance-marker() = _command("no-relevance-marker", (:)) +#let logo-color(residues, color) = _command("logo-color", (residues: residues, color: color)) +#let clear-logo-colors(default: "Black") = _command("clear-logo-colors", (default: default)) +#let legend-track(color: "Black") = _command("legend-track", (color: color)) +#let no-legend-track() = _command("no-legend-track", (:)) +#let legend-color(color) = _command("legend-color", (color: color)) +#let legend-offset(dx, dy) = _command("legend-offset", (dx: dx, dy: dy)) +#let color-swatch(color) = box(width: 10pt, height: 10pt, fill: resolve-color(color), stroke: none)[] +#let show-structure-types(format, types) = _command("show-structure-types", (format: format, types: types)) +#let hide-structure-types(format, types) = _command("hide-structure-types", (format: format, types: types)) +#let structure-appearance(format, structure-type, position, style, text) = _command("structure-appearance", (format: format, structure-type: structure-type, position: position, style: style, text: text)) +#let use-first-dssp-column() = _command("use-first-dssp-column", (:)) +#let use-second-dssp-column() = _command("use-second-dssp-column", (:)) +#let stride-track(sequence, source) = _command("stride-track", (sequence: sequence, source: source)) +#let dssp-track(sequence, source) = _command("dssp-track", (sequence: sequence, source: source)) +#let hmmtop-track(sequence, source, source-sequence: none) = _command("hmmtop-track", (sequence: sequence, source: source, source-sequence: source-sequence)) +#let phd-topology-track(sequence, source) = _command("phd-topology-track", (sequence: sequence, source: source)) +#let phd-secondary-track(sequence, source) = _command("phd-secondary-track", (sequence: sequence, source: source)) +#let consensus-from-sequence(sequence) = _command("consensus-from-sequence", (sequence: sequence)) +#let consensus-from-all-sequences() = _command("consensus-from-all-sequences", (:)) +#let show-leading-gaps() = _command("show-leading-gaps", (:)) +#let hide-leading-gaps() = _command("hide-leading-gaps", (:)) +#let shift-single-sequence(..args) = { + let positional = args.pos() + let value = if positional.len() > 0 { positional.first() } else { args.named().at("value", default: -1) } + _command("shift-single-sequence", (value: value)) +} +#let keep-single-sequence-gaps() = _command("keep-single-sequence-gaps", (:)) +#let hide-residues() = _command("hide-residues", (:)) +#let show-residues() = _command("show-residues", (:)) +#let bar-graph-stretch(value, position: none) = _command("bar-graph-stretch", (value: value, position: position)) +#let color-scale-stretch(value, position: none) = _command("color-scale-stretch", (value: value, position: position)) +#let alignment(position) = _command("alignment", (position: position)) +#let character-stretch(value) = _command("character-stretch", (value: value)) +#let line-stretch(value) = _command("line-stretch", (value: value)) +#let numbering-width(digits) = _command("numbering-width", (digits: digits)) +#let fingerprint(value) = _command("fingerprint", (value: value)) +#let align-right-labels() = _command("align-right-labels", (:)) +#let align-left-labels() = _command("align-left-labels", (:)) +#let text-family(target, family) = _command("text-family", (target: target, value: family)) +#let text-weight(target, weight) = _command("text-weight", (target: target, value: weight)) +#let text-posture(target, posture) = _command("text-posture", (target: target, value: posture)) +#let text-size(target, size) = _command("text-size", (target: target, value: size)) +#let text-style(target, family, weight, posture, size) = _command("text-style", (target: target, family: family, weight: weight, posture: posture, size: size)) +#let caption(text, position: "bottom") = _command("caption", (text: text, position: position)) +#let short-caption(text) = _command("short-caption", (text: text)) +#let small-separator() = _command("separator-space", (value: 3pt)) +#let medium-separator() = _command("separator-space", (value: 6pt)) +#let large-separator() = _command("separator-space", (value: 12pt)) +#let export-consensus(sequence, filename, format: "chimera") = _command("export-consensus", (sequence: sequence, filename: filename, format: format)) +#let pdb-selection(selection) = pdb-selection-list(selection) +#let no-block-gap() = _command("block-gap", (value: 0pt)) +#let small-block-gap() = _command("block-gap-factor", (value: 1.0)) +#let medium-block-gap() = _command("block-gap-factor", (value: 1.5)) +#let large-block-gap() = _command("block-gap-factor", (value: 2.0)) +#let block-gap(value) = _command("block-gap", (value: value)) +#let flexible-block-gap() = _command("block-space-mode", (fixed: false)) +#let fixed-block-gap() = _command("block-space-mode", (fixed: true)) +#let no-line-gap() = _command("line-gap", (value: 0pt)) +#let small-line-gap() = _command("line-gap", (value: 3pt)) +#let medium-line-gap() = _command("line-gap", (value: 6pt)) +#let large-line-gap() = _command("line-gap", (value: 12pt)) +#let line-gap(value) = _command("line-gap", (value: value)) +#let feature-slot-space(position, value) = _command("feature-slot-space", (position: position, value: value)) + +#let _apply-command(config, command) = { + let kind = command.at("kind") + if kind == "sequence-type" { + config.insert("seq-type", _upper(str(command.at("value")))) + } else if kind == "scoring-mode" { + config.at("shading").insert("mode", command.at("mode")) + config.at("shading").insert("option", command.at("option")) + if command.at("mode") == "diverse" or command.at("mode") == "singleseq" { + let reference = command.at("option", default: none) + config.at("shading").insert("reference", if reference == none { 1 } else { reference }) + } else if command.at("mode") == "T-Coffee" and command.at("option") != none { + config.at("tcoffee").insert("source", command.at("option")) + config.at("tcoffee").insert("scores", read-tcoffee(command.at("option"))) + } + } else if kind == "color-scheme" { + config.at("shading").insert("scheme", command.at("name")) + _apply-shading-colors(config, command.at("name")) + } else if kind == "define-color-scheme" { + config.at("shading-schemes").insert(command.at("name"), _style-snapshot(config)) + } else if kind == "tcoffee-scores" { + config.at("tcoffee").insert("source", command.at("source")) + config.at("tcoffee").insert("scores", read-tcoffee(command.at("source"))) + } else if kind == "sequence-logo-track" { + config.at("sequence-logo").insert("show", true) + config.at("sequence-logo").insert("position", command.at("position")) + config.at("sequence-logo").insert("colorset", command.at("colorset")) + } else if kind == "no-sequence-logo-track" { + config.at("sequence-logo").insert("show", false) + } else if kind == "frequency-correction" { + config.insert("frequency-correction", true) + } else if kind == "no-frequency-correction" { + config.insert("frequency-correction", false) + } else if kind == "subfamily" { + config.insert("subfamily", command.at("sequences")) + } else if kind == "subfamily-logo-track" { + config.at("subfamily-logo").insert("show", true) + config.at("subfamily-logo").insert("position", command.at("position")) + config.at("subfamily-logo").insert("colorset", command.at("colorset")) + } else if kind == "no-subfamily-logo-track" { + config.at("subfamily-logo").insert("show", false) + } else if kind == "sequence-logo-name" { + config.at("sequence-logo").insert("name", command.at("name")) + } else if kind == "subfamily-logo-name" { + config.at("subfamily-logo").insert("name", command.at("name")) + if command.at("negative-name") != none { + config.at("subfamily-logo").insert("negative-name", command.at("negative-name")) + } + } else if kind == "logo-scale" { + config.at("logo-scale").insert("show", true) + config.at("logo-scale").insert("position", command.at("position")) + config.at("logo-scale").insert("color", command.at("color")) + } else if kind == "no-logo-scale" { + config.at("logo-scale").insert("show", false) + } else if kind == "logo-stretch" { + config.at("sequence-logo").insert("stretch", command.at("value")) + } else if kind == "negative-logo-values" { + config.at("subfamily-logo").insert("show-negatives", true) + } else if kind == "no-negative-logo-values" { + config.at("subfamily-logo").insert("show-negatives", false) + } else if kind == "relevance-threshold" { + config.at("relevance").insert("threshold", command.at("value")) + } else if kind == "relevance-marker" { + config.at("relevance").insert("show", true) + config.at("relevance").insert("char", command.at("char")) + config.at("relevance").insert("color", command.at("color")) + } else if kind == "no-relevance-marker" { + config.at("relevance").insert("show", false) + } else if kind == "logo-color" { + for residue in _chars(command.at("residues")) { + config.at("logo-colors").at("map").insert(_upper(residue), command.at("color")) + } + } else if kind == "clear-logo-colors" { + config.at("logo-colors").insert("default", command.at("default")) + config.at("logo-colors").insert("map", (:)) + } else if kind == "legend-track" { + config.at("legend").insert("show", true) + config.at("legend").insert("color", command.at("color")) + } else if kind == "no-legend-track" { + config.at("legend").insert("show", false) + } else if kind == "legend-color" { + config.at("legend").insert("color", command.at("color")) + } else if kind == "legend-offset" { + config.at("legend").insert("dx", command.at("dx")) + config.at("legend").insert("dy", command.at("dy")) + } else if kind == "show-structure-types" { + config.at("structure-show").insert(command.at("format"), _type-list(command.at("types"))) + } else if kind == "hide-structure-types" { + _hide-structure-types(config, command.at("format"), command.at("types")) + } else if kind == "structure-appearance" { + config.at("structure-appearance").insert(command.at("format") + ":" + command.at("structure-type"), ("position": command.at("position"), "style": command.at("style"), "text": command.at("text"))) + } else if kind == "use-first-dssp-column" { + config.insert("dssp-second-column", false) + } else if kind == "use-second-dssp-column" { + config.insert("dssp-second-column", true) + } else if kind == "stride-track" { + config.at("structure-data").push(("kind": "STRIDE", "sequence": command.at("sequence"), "data": read-stride(command.at("source")))) + } else if kind == "dssp-track" { + config.at("structure-data").push(("kind": "DSSP", "sequence": command.at("sequence"), "data": read-dssp(command.at("source"), use-second-column: config.at("dssp-second-column")))) + } else if kind == "hmmtop-track" { + config.at("structure-data").push(("kind": "HMMTOP", "sequence": command.at("sequence"), "data": read-hmmtop(command.at("source"), sequence: command.at("source-sequence", default: none)))) + } else if kind == "phd-topology-track" { + config.at("structure-data").push(("kind": "PHDtopo", "sequence": command.at("sequence"), "data": read-phd-topology(command.at("source")))) + } else if kind == "phd-secondary-track" { + config.at("structure-data").push(("kind": "PHDsec", "sequence": command.at("sequence"), "data": read-phd-secondary(command.at("source")))) + } else if kind == "threshold" { + config.insert("threshold", command.at("value")) + } else if kind == "shade-all-residues" { + config.insert("threshold", 0) + } else if kind == "all-match-threshold" { + config.insert("allmatch-threshold", command.at("value")) + } else if kind == "disable-all-match-threshold" { + config.insert("allmatch-threshold", 101) + } else if kind == "hide-all-match-positions" { + config.insert("hide-allmatch-positions", true) + } else if kind == "show-all-match-positions" { + config.insert("hide-allmatch-positions", false) + } else if kind == "weight-table" { + let name = str(command.at("name")) + config.insert("weight-table", name) + if name == "PAM250" { + config.insert("gap-penalty", -8) + } else if name == "PAM100" { + config.insert("gap-penalty", -9) + } else { + config.insert("gap-penalty", 0) + } + } else if kind == "set-weight" { + let a = _upper(str(command.at("residue-a"))) + let b = _upper(str(command.at("residue-b"))) + config.at("custom-weights").insert(a + ":" + b, command.at("value")) + config.at("custom-weights").insert(b + ":" + a, command.at("value")) + } else if kind == "gap-penalty" { + config.insert("gap-penalty", command.at("value")) + } else if kind == "residues-per-line" { + config.insert("residues-per-line", command.at("value")) + } else if kind == "auto-layout" { + config.at("auto-layout").insert("fit", command.at("fit")) + config.at("auto-layout").insert("min", command.at("min")) + config.at("auto-layout").insert("max", command.at("max")) + if command.at("fit") != none and command.at("fit") != false { + config.insert("residues-per-line", auto) + } + } else if kind == "auto-page" { + config.at("auto-page").insert("enabled", true) + config.at("auto-page").insert("blocks", command.at("blocks")) + config.at("auto-page").insert("repeat-legend", command.at("repeat-legend")) + } else if kind == "numbering-track" { + let pos = command.at("position") + config.at("numbering").insert("show", true) + config.at("numbering").insert("left", pos == "left" or pos == "leftright") + config.at("numbering").insert("right", pos == "right" or pos == "leftright") + if command.at("color") != none { + config.insert("numbering-color", command.at("color")) + } + } else if kind == "no-numbering-track" { + config.at("numbering").insert("show", false) + } else if kind == "names-track" { + config.at("names").insert("show", true) + config.at("names").insert("position", command.at("position")) + if command.at("color") != none { + config.insert("names-color", command.at("color")) + } + } else if kind == "no-names-track" { + config.at("names").insert("show", false) + } else if kind == "sequence-name" { + config.at("sequence-names").insert(str(command.at("sequence")), command.at("name")) + } else if kind == "names-color" { + config.insert("names-color", command.at("color")) + } else if kind == "sequence-name-color" { + for ref in _ref-list(command.at("sequences")) { + config.at("sequence-name-colors").insert(str(ref), command.at("color")) + } + } else if kind == "hide-sequence-name" { + for ref in _ref-list(command.at("sequences")) { + config.at("hidden-names").push(ref) + } + } else if kind == "numbering-color" { + config.insert("numbering-color", command.at("color")) + } else if kind == "sequence-number-color" { + for ref in _ref-list(command.at("sequences")) { + config.at("sequence-number-colors").insert(str(ref), command.at("color")) + } + } else if kind == "hide-sequence-number" { + for ref in _ref-list(command.at("sequences")) { + config.at("hidden-numbers").push(ref) + } + } else if kind == "consensus-track" { + config.at("consensus").insert("show", true) + config.at("consensus").insert("position", command.at("position")) + config.at("consensus").insert("scale", command.at("scale")) + if command.at("name") != none { + config.at("consensus").insert("name", command.at("name")) + } + } else if kind == "no-consensus-track" { + config.at("consensus").insert("show", false) + } else if kind == "consensus-name" { + config.at("consensus").insert("name", command.at("name")) + } else if kind == "language" { + let name = command.at("name") + config.at("consensus").insert("name", if name == "german" { "Konsensus" } else if name == "spanish" { "consenso" } else { "consensus" }) + } else if kind == "consensus-symbols" { + config.at("consensus").insert("symbols", ("none": command.at("none"), "conserved": command.at("conserved"), "allmatch": command.at("allmatch"))) + } else if kind == "consensus-colors" { + config.at("consensus").insert("colors", ( + "none": (fg: command.at("none-fg"), bg: command.at("none-bg")), + "conserved": (fg: command.at("conserved-fg"), bg: command.at("conserved-bg")), + "allmatch": (fg: command.at("allmatch-fg"), bg: command.at("allmatch-bg")), + )) + } else if kind == "ruler-track" { + config.at("ruler").insert("show", true) + config.at("ruler").insert("position", command.at("position")) + config.at("ruler").insert("sequence", command.at("sequence")) + if command.at("steps") != none { + config.at("ruler").insert("steps", command.at("steps")) + } + if command.at("color") != none { + config.at("ruler").insert("color", command.at("color")) + config.at("ruler-colors").insert("top", command.at("color")) + config.at("ruler-colors").insert("bottom", command.at("color")) + } + } else if kind == "no-ruler-track" { + let pos = command.at("position") + if pos == none or pos == config.at("ruler").at("position") { + config.at("ruler").insert("show", false) + } + } else if kind == "ruler-steps" { + let pos = command.at("position") + if pos == none or pos == config.at("ruler").at("position") { + config.at("ruler").insert("steps", command.at("value")) + } + } else if kind == "ruler-color" { + let pos = command.at("position") + if pos == none { + config.at("ruler").insert("color", command.at("color")) + config.at("ruler-colors").insert("top", command.at("color")) + config.at("ruler-colors").insert("bottom", command.at("color")) + } else { + config.at("ruler-colors").insert(pos, command.at("color")) + } + } else if kind == "ruler-name" { + let pos = command.at("position") + if pos == none { + config.at("ruler-names").insert("top", command.at("name")) + config.at("ruler-names").insert("bottom", command.at("name")) + } else { + config.at("ruler-names").insert(pos, command.at("name")) + } + } else if kind == "ruler-name-color" { + let pos = command.at("position") + if pos == none { + config.at("ruler-name-colors").insert("top", command.at("color")) + config.at("ruler-name-colors").insert("bottom", command.at("color")) + } else { + config.at("ruler-name-colors").insert(pos, command.at("color")) + } + } else if kind == "ruler-marker" { + let pos = command.at("position") + let entry = (text: command.at("text"), color: command.at("color")) + if pos == none { + config.at("ruler-labels").at("top").insert(str(command.at("number")), entry) + config.at("ruler-labels").at("bottom").insert(str(command.at("number")), entry) + } else { + config.at("ruler-labels").at(pos).insert(str(command.at("number")), entry) + } + } else if kind == "ruler-space" { + let pos = command.at("position") + if pos == none { + config.at("ruler-spacing").insert("top", command.at("value")) + config.at("ruler-spacing").insert("bottom", command.at("value")) + } else { + config.at("ruler-spacing").insert(pos, command.at("value")) + } + } else if kind == "rotate-ruler" { + let pos = command.at("position") + if pos == none or pos == "n" { + config.at("ruler-rotation").insert("top", true) + config.at("ruler-rotation").insert("bottom", true) + } else { + config.at("ruler-rotation").insert(pos, true) + } + } else if kind == "unrotate-ruler" { + let pos = command.at("position") + if pos == none or pos == "n" { + config.at("ruler-rotation").insert("top", false) + config.at("ruler-rotation").insert("bottom", false) + } else { + config.at("ruler-rotation").insert(pos, false) + } + } else if kind == "start-number" { + config.at("start-numbers").insert(str(command.at("sequence")), command.at("start")) + if command.at("selection") != none { + config.at("sequence-windows").push((kind: "sequence-window", sequence: command.at("sequence"), selection: command.at("selection"), start: none)) + } + } else if kind == "allow-zero-numbering" { + config.insert("allow-zero", true) + } else if kind == "disallow-zero-numbering" { + config.insert("allow-zero", false) + } else if kind == "sequence-length" { + config.at("sequence-lengths").insert(str(command.at("sequence")), command.at("length")) + } else if kind == "sequence-window" { + if command.at("start") != none { + config.at("start-numbers").insert(str(command.at("sequence")), command.at("start")) + } + config.at("sequence-windows").push(command) + } else if kind == "region-highlight" { + config.at("shade-regions").push(command) + } else if kind == "region-tint" { + config.at("tint-regions").push(command) + } else if kind == "tint-default" { + config.insert("tint-default", command.at("effect")) + } else if kind == "region-lower" { + config.at("lower-regions").push(command) + } else if kind == "region-emphasis" { + config.at("emph-regions").push(command) + } else if kind == "emphasis-default" { + config.insert("emph-default", command.at("style")) + } else if kind == "frame-block" { + config.at("frame-regions").push(command) + } else if kind == "hide-sequence" { + for ref in _ref-list(command.at("sequence")) { + config.at("hidden").push(ref) + } + } else if kind == "hide-all-sequences" { + config.insert("hide-seqs", true) + } else if kind == "show-all-sequences" { + config.insert("hide-seqs", false) + } else if kind == "remove-sequence" { + for ref in _ref-list(command.at("sequence")) { + config.at("killed").push(ref) + } + } else if kind == "no-shade" { + for ref in _ref-list(command.at("sequences")) { + config.at("no-shade").push(ref) + } + } else if kind == "separation-line" { + for ref in _ref-list(command.at("sequence")) { + config.at("separation-lines").push(ref) + } + } else if kind == "sequence-order" { + config.insert("order", _ref-list(command.at("order"))) + } else if kind == "feature" { + config.at("features").push(command) + } else if kind == "feature-rule" { + config.insert("feature-rule", command.at("thickness")) + } else if kind == "codon" { + let residue = _upper(str(command.at("residue"))) + let choices = str(command.at("triplets")).split(",").map(item => item.trim()).filter(item => item != "") + if choices.len() > 0 { + config.at("genetic-code").insert(residue, choices.last()) + } + } else if kind == "genetic-code" { + let name = str(command.at("name")) + config.insert("genetic-code", _standard-genetic-code + if name == "ciliate" { (Q: "YAR") } else { (:) }) + } else if kind == "backtranslation-label" { + config.at("backtranslation").insert("label-style", command.at("style")) + config.at("backtranslation").insert("label-size", command.at("size")) + } else if kind == "backtranslation-text" { + config.at("backtranslation").insert("text-style", command.at("style")) + config.at("backtranslation").insert("text-size", command.at("size")) + } else if kind == "feature-text-label" { + config.at("feature-text-names").insert(command.at("position"), command.at("name")) + } else if kind == "feature-style-label" { + config.at("feature-style-names").insert(command.at("position"), command.at("name")) + } else if kind == "hide-feature-text-label" { + config.at("feature-text-names").insert(command.at("position"), "") + } else if kind == "hide-feature-style-label" { + config.at("feature-style-names").insert(command.at("position"), "") + } else if kind == "hide-feature-text-labels" { + config.insert("feature-text-names", (:)) + } else if kind == "hide-feature-style-labels" { + config.insert("feature-style-names", (:)) + } else if kind == "feature-text-label-color" { + config.at("feature-text-name-colors").insert("default", command.at("color")) + } else if kind == "feature-style-label-color" { + config.at("feature-style-name-colors").insert("default", command.at("color")) + } else if kind == "feature-text-label-color-at" { + config.at("feature-text-name-colors").insert(command.at("position"), command.at("color")) + } else if kind == "feature-style-label-color-at" { + config.at("feature-style-name-colors").insert(command.at("position"), command.at("color")) + } else if kind == "consensus-from-sequence" { + config.at("consensus").insert("source", command.at("sequence")) + } else if kind == "consensus-from-all-sequences" { + config.at("consensus").insert("source", "all") + } else if kind == "show-leading-gaps" { + config.insert("show-leading-gaps", true) + } else if kind == "hide-leading-gaps" { + config.insert("show-leading-gaps", false) + } else if kind == "shift-single-sequence" { + config.insert("single-seq-shift", command.at("value")) + } else if kind == "keep-single-sequence-gaps" { + config.insert("keep-single-seq-gaps", true) + } else if kind == "hide-residues" { + config.insert("hide-residues", true) + } else if kind == "show-residues" { + config.insert("hide-residues", false) + } else if kind == "gap-char" { + config.at("gaps").insert("char", command.at("symbol")) + } else if kind == "gap-rule" { + config.at("gaps").insert("rule-thickness", command.at("thickness")) + } else if kind == "gap-colors" { + config.at("gaps").insert("fg", command.at("foreground")) + config.at("gaps").insert("bg", command.at("background")) + } else if kind == "stop-char" { + config.insert("stop-char", command.at("symbol")) + } else if kind == "peptide-groups" { + config.insert("pep-groups", _group-list(command.at("groups"))) + } else if kind == "dna-groups" { + config.insert("dna-groups", _group-list(command.at("groups"))) + } else if kind == "peptide-similarities" { + config.at("pep-sims").insert(_upper(str(command.at("residue"))), _upper(str(command.at("similars")))) + } else if kind == "dna-similarities" { + config.at("dna-sims").insert(_upper(str(command.at("residue"))), _upper(str(command.at("similars")))) + } else if kind == "residue-style" { + config.at("residue-style").insert(command.at("target"), _style-record(command.at("fg"), command.at("bg"), command.at("case"), command.at("style"))) + } else if kind == "cell-style" { + config.at("cell-styles").push(command.at("callback")) + } else if kind == "clear-functional-groups" { + config.at("functional-groups").insert("custom", ()) + config.insert("functional-default", "custom") + } else if kind == "functional-group" { + let groups = config.at("functional-groups").at("custom", default: ()) + groups.push(_func-style-record(command.at("name"), command.at("residues"), command.at("fg"), command.at("bg"), command.at("case"), command.at("style"))) + config.at("functional-groups").insert("custom", groups) + config.insert("functional-default", "custom") + } else if kind == "functional-style" { + config.at("functional-style-overrides").insert(_upper(str(command.at("residue"))), _style-record(command.at("fg"), command.at("bg"), command.at("case"), command.at("style"))) + } else if kind == "bar-graph-stretch" { + let key = if command.at("position") == none { "default" } else { command.at("position") } + config.at("bar-graph-stretch").insert(key, command.at("value")) + } else if kind == "color-scale-stretch" { + let key = if command.at("position") == none { "default" } else { command.at("position") } + config.at("color-scale-stretch").insert(key, command.at("value")) + } else if kind == "alignment" { + config.insert("alignment", command.at("position")) + } else if kind == "character-stretch" { + config.insert("char-stretch", command.at("value")) + } else if kind == "line-stretch" { + config.insert("line-gap", config.at("font-size") * command.at("value")) + } else if kind == "numbering-width" { + config.insert("numbering-width-digits", command.at("digits")) + } else if kind == "fingerprint" { + config.insert("residues-per-line", command.at("value")) + config.at("names").insert("show", true) + config.at("names").insert("position", "left") + config.at("numbering").insert("show", false) + config.at("ruler").insert("steps", 100) + config.at("residue-style").insert("nomatch", _style-record("Black", "Gray10", "upper", "normal")) + } else if kind == "align-right-labels" { + config.insert("align-right-labels", true) + } else if kind == "align-left-labels" { + config.insert("align-right-labels", false) + } else if kind == "text-family" { + _set-text-style(config, command.at("target"), "family", command.at("value")) + } else if kind == "text-weight" { + _set-text-style(config, command.at("target"), "weight", command.at("value")) + } else if kind == "text-posture" { + _set-text-style(config, command.at("target"), "style", command.at("value")) + } else if kind == "text-size" { + _set-text-style(config, command.at("target"), "size", command.at("value")) + } else if kind == "text-style" { + _set-text-style(config, command.at("target"), "family", command.at("family")) + _set-text-style(config, command.at("target"), "weight", command.at("weight")) + _set-text-style(config, command.at("target"), "style", command.at("posture")) + _set-text-style(config, command.at("target"), "size", command.at("size")) + } else if kind == "caption" { + config.at("captions").insert(command.at("position"), command.at("text")) + } else if kind == "short-caption" { + config.at("captions").insert("short", command.at("text")) + } else if kind == "separator-space" { + config.insert("separation-space", command.at("value")) + } else if kind == "export-consensus" { + config.insert("exported-consensus", command) + } else if kind == "block-gap" { + config.insert("block-gap", command.at("value")) + } else if kind == "block-gap-factor" { + config.insert("block-gap", config.at("font-size") * command.at("value")) + } else if kind == "block-space-mode" { + config.insert("fixed-block-space", command.at("fixed")) + } else if kind == "line-gap" { + config.insert("line-gap", command.at("value")) + } else if kind == "feature-slot-space" { + config.at("feature-slot-spacing").insert(command.at("position"), command.at("value")) + } + config +} diff --git a/packages/preview/typshade/0.1.3/internal/engine/layout.typ b/packages/preview/typshade/0.1.3/internal/engine/layout.typ new file mode 100644 index 0000000000..f0039ed707 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/engine/layout.typ @@ -0,0 +1,394 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../model/pdb.typ": _pdb-selection-positions +#import "../model/parser.typ": _chars, _lower, _upper + +#let _motif-pattern(selection) = { + let text = str(selection).trim() + if text == "" or text == "all" or text.contains("..") or text.contains(":") or text.matches(regex("^\\d+(?:\\s*,\\s*\\d+)*$")).len() > 0 { + return none + } + let pattern = () + let idx = 0 + while idx < text.len() { + let ch = text.slice(idx, idx + 1) + if ch == "[" { + let rest = text.slice(idx + 1) + let close = rest.position("]") + if close == none { + return none + } + pattern.push(_upper(rest.slice(0, close))) + idx += close + 2 + } else if ch == "X" or ch == "x" { + pattern.push(none) + idx += 1 + } else if ch.matches(regex("[A-Za-z]")).len() > 0 { + pattern.push(_upper(ch)) + idx += 1 + } else { + return none + } + } + pattern +} + +#let _motif-columns(seq, selection) = { + let pattern = _motif-pattern(selection) + if pattern == none { + return none + } + let residues = () + let columns = () + for (idx, ch) in _chars(seq.at("aligned")).enumerate() { + if ch == "." or ch == "-" { + continue + } + residues.push(_upper(ch)) + columns.push(idx) + } + let out = () + if pattern.len() == 0 or residues.len() < pattern.len() { + return out + } + for start in range(0, residues.len() - pattern.len() + 1) { + let matched = true + for offset in range(0, pattern.len()) { + let allowed = pattern.at(offset) + if allowed != none and not allowed.contains(residues.at(start + offset)) { + matched = false + } + } + if matched { + for offset in range(0, pattern.len()) { + out.push(columns.at(start + offset)) + } + } + } + out +} + +#let _selection-fail(selection, reason) = { + assert(false, message: "typshade: invalid selection `" + str(selection) + "`: " + reason) +} + +#let _is-gap(char) = char == "." or char == "-" + +#let _all-columns(seq) = { + let out = () + for idx in range(0, seq.at("aligned").len()) { + out.push(idx) + } + out +} + +#let _set-key(item) = str(item) + +#let _contains-column(items, column) = { + let key = _set-key(column) + for item in items { + if _set-key(item) == key { + return true + } + } + false +} + +#let _union-columns(groups) = { + let out = () + let seen = (:) + for group in groups { + for column in group { + let key = _set-key(column) + if not seen.keys().contains(key) { + seen.insert(key, true) + out.push(column) + } + } + } + out.sorted() +} + +#let _intersection-columns(groups) = { + if groups.len() == 0 { + return () + } + let out = () + for column in groups.first() { + let present = true + for group in groups.slice(1) { + if not _contains-column(group, column) { + present = false + } + } + if present and not _contains-column(out, column) { + out.push(column) + } + } + out.sorted() +} + +#let _difference-columns(base, remove) = base.filter(column => not _contains-column(remove, column)) + +#let _selection-columns-from-positions(seq, positions) = { + let wanted = if type(positions) == array { positions } else { (positions,) } + let out = () + for (idx, mapped) in seq.at("positions").enumerate() { + if mapped != none and wanted.contains(mapped) { + out.push(idx) + } + } + out +} + +#let _selection-columns-from-range(seq, start, stop) = { + let out = () + let low = calc.min(start, stop) + let high = calc.max(start, stop) + for (idx, pos) in seq.at("positions").enumerate() { + if pos != none and pos >= low and pos <= high { + out.push(idx) + } + } + out +} + +#let _column-residues(alignment, column) = { + let residues = () + for sequence in alignment.at("sequences") { + let ch = _upper(sequence.at("aligned").slice(column, column + 1)) + if not _is-gap(ch) { + residues.push(ch) + } + } + residues +} + +#let _column-metric(alignment, column, metric) = { + let name = _lower(str(metric)) + let count = alignment.at("sequences").len() + let residues = _column-residues(alignment, column) + let covered = residues.len() + if name == "coverage" { + if count == 0 { 0.0 } else { covered * 100.0 / count } + } else if name == "gap-fraction" or name == "gaps" { + if count == 0 { 0.0 } else { (count - covered) * 100.0 / count } + } else if name == "entropy" { + if covered == 0 { + 0.0 + } else { + let counts = (:) + for residue in residues { + counts.insert(residue, counts.at(residue, default: 0) + 1) + } + let total = 0.0 + for value in counts.values() { + let p = value / covered + total -= p * calc.log(p) / calc.log(2) + } + total + } + } else if name == "conservation" or name == "identity" { + if covered == 0 { + 0.0 + } else { + let counts = (:) + for residue in residues { + counts.insert(residue, counts.at(residue, default: 0) + 1) + } + let max-count = 0 + for value in counts.values() { + if value > max-count { + max-count = value + } + } + max-count * 100.0 / covered + } + } else { + _selection-fail((kind: "typshade-selection", op: "metric", metric: metric), "unknown metric `" + str(metric) + "`") + } +} + +#let _metric-passes(value, spec) = { + if spec.at("above", default: none) != none and not (value > spec.at("above")) { + return false + } + if spec.at("below", default: none) != none and not (value < spec.at("below")) { + return false + } + if spec.at("at-least", default: spec.at("min", default: none)) != none and value < spec.at("at-least", default: spec.at("min", default: none)) { + return false + } + if spec.at("at-most", default: spec.at("max", default: none)) != none and value > spec.at("at-most", default: spec.at("max", default: none)) { + return false + } + if spec.at("equals", default: none) != none and value != spec.at("equals") { + return false + } + true +} + +#let _selection-columns-from-metric(seq, selection, alignment, resolve) = { + assert(alignment != none, message: "typshade: metric selections require alignment context") + let base = resolve(selection.at("selection", default: "all")) + let out = () + for column in base { + let value = _column-metric(alignment, column, selection.at("metric", default: selection.at("name", default: "conservation"))) + if _metric-passes(value, selection) { + out.push(column) + } + } + out +} + +#let _pad-selection-columns(seq, columns, before, after) = { + if columns.len() == 0 { + return () + } + let first = none + let last = none + for column in columns { + let pos = seq.at("positions").at(column) + if pos != none { + if first == none or pos < first { + first = pos + } + if last == none or pos > last { + last = pos + } + } + } + if first == none or last == none { + return columns + } + _selection-columns-from-range(seq, calc.max(1, first - before), last + after) +} + +#let _selection-dsl-columns(seq, selection, alignment: none, resolve: none) = { + let op = selection.at("op", default: "or") + if op == "all" { + return _all-columns(seq) + } + if op == "range" { + let range-value = selection.at("range", default: none) + if range-value != none { + return resolve(range-value) + } + return _selection-columns-from-range(seq, int(selection.at("start")), int(selection.at("stop", default: selection.at("start")))) + } + if op == "positions" { + return _selection-columns-from-positions(seq, selection.at("positions")) + } + if op == "motif" { + let columns = _motif-columns(seq, selection.at("pattern")) + return if columns == none { () } else { columns } + } + if op == "metric" { + return _selection-columns-from-metric(seq, selection, alignment, resolve) + } + if op == "not" { + return _difference-columns(_all-columns(seq), resolve(selection.at("selection"))) + } + if op == "pad" { + let before = int(selection.at("before", default: selection.at("padding", default: 0))) + let after = int(selection.at("after", default: before)) + return _pad-selection-columns(seq, resolve(selection.at("selection")), before, after) + } + let items = selection.at("items", default: ()) + let groups = items.map(item => resolve(item)) + let columns = if op == "and" { + _intersection-columns(groups) + } else if op == "or" or op == "select" { + _union-columns(groups) + } else { + _selection-fail(selection, "unknown selection operation `" + str(op) + "`") + } + let padding = int(selection.at("padding", default: 0)) + if padding != 0 { + _pad-selection-columns(seq, columns, padding, padding) + } else { + columns + } +} + +#let _selection-columns(seq, selection, alignment: none) = { + if selection == none { + return () + } + if type(selection) == dictionary and selection.at("kind", default: none) == "typshade-selection" { + return _selection-dsl-columns(seq, selection, alignment: alignment, resolve: item => _selection-columns(seq, item, alignment: alignment)) + } + let pdb-positions = _pdb-selection-positions(selection) + if pdb-positions != none { + let values = () + for (idx, mapped) in seq.at("positions").enumerate() { + if mapped != none and pdb-positions.contains(mapped) { + values.push(idx) + } + } + return values + } + let motif-columns = _motif-columns(seq, selection) + if motif-columns != none { + return motif-columns + } + let text = str(selection).trim() + if text.trim() == "all" { + return _all-columns(seq) + } + if text.contains(":") { + _selection-fail(selection, "expected a valid PDB selection such as point[1]:source,1[CA]") + } + if text.matches(regex("[A-Za-z\\[]")).len() > 0 { + _selection-fail(selection, "expected a residue range/list or a valid motif pattern") + } + let values = () + for part in text.split(",") { + let trimmed = part.trim() + if trimmed == "" { + continue + } + if trimmed.contains("..") { + let bounds = trimmed.split("..") + assert( + bounds.len() == 2 and bounds.first().matches(regex("^\\d+$")).len() > 0 and bounds.last().matches(regex("^\\d+$")).len() > 0, + message: "typshade: invalid selection `" + str(selection) + "`: ranges must look like 1..10", + ) + values += _selection-columns-from-range(seq, int(bounds.first()), int(bounds.last())) + } else { + assert( + trimmed.matches(regex("^\\d+$")).len() > 0, + message: "typshade: invalid selection `" + str(selection) + "`: entries must be residue numbers, ranges, motifs, or all", + ) + values += _selection-columns-from-positions(seq, int(trimmed)) + } + } + values +} + +#let _sorted-unique(items) = { + let seen = (:) + let out = () + for item in items { + let key = str(item) + if not seen.keys().contains(key) { + seen.insert(key, true) + out.push(item) + } + } + out.sorted() +} + +#let _empty-cell() = (char: "", fg: "Black", bg: none, emph: false, frame: none) + +#let _ordinal-label(index) = { + let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if index <= letters.len() { + letters.slice(index - 1, index) + } else { + str(index) + } +} + +#let _selection-string(range) = str(range.at(0)) + ".." + str(range.at(1)) diff --git a/packages/preview/typshade/0.1.3/internal/interface/analysis.typ b/packages/preview/typshade/0.1.3/internal/interface/analysis.typ new file mode 100644 index 0000000000..6198bff61b --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/analysis.typ @@ -0,0 +1,185 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/layout.typ" as _layout +#import "../model/logo.typ" as _logo +#import "../model/palette.typ" as _palette +#import "../model/parser.typ" as _parser + +#let _upper(text) = { + let lower = "abcdefghijklmnopqrstuvwxyz" + let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + let out = "" + for ch in str(text).clusters() { + let idx = lower.position(ch) + out += if idx == none { ch } else { upper.slice(idx, idx + 1) } + } + out +} + +#let _is-gap(value) = { + let ch = str(value) + ch == "" or ch == "." or ch == "-" +} + +#let _sequence-index(alignment, sequence) = _logo._resolve-sequence(alignment, sequence) + +#let _selected-columns(alignment, reference, selection) = { + if selection == none or selection == "all" { + return range(0, alignment.at("columns")) + } + let ref-index = _sequence-index(alignment, reference) + _layout._selection-columns(alignment.at("sequences").at(ref-index), selection, alignment: alignment) +} + +#let _similar-residue(a, b, seq-type, similarities: none, groups: none) = { + let left = _upper(a) + let right = _upper(b) + if left == right { + return true + } + let resolved-sims = if similarities != none { + similarities + } else if seq-type == "N" { + _palette._dna-sims + } else { + _palette._pep-sims + } + let left-sims = resolved-sims.at(left, default: "") + let right-sims = resolved-sims.at(right, default: "") + if str(left-sims).contains(right) or str(right-sims).contains(left) { + return true + } + let resolved-groups = if groups != none { + groups + } else if seq-type == "N" { + _palette._dna-groups + } else { + _palette._pep-groups + } + for group in resolved-groups { + let text = _upper(group) + if text.contains(left) and text.contains(right) { + return true + } + } + false +} + +#let _percent-score( + alignment, + sequence-a, + sequence-b, + kind, + selection: "all", + reference: auto, + seq-type: auto, + similarities: none, + groups: none, +) = { + let left-index = _sequence-index(alignment, sequence-a) + let right-index = _sequence-index(alignment, sequence-b) + let left = alignment.at("sequences").at(left-index) + let right = alignment.at("sequences").at(right-index) + let resolved-reference = if reference == auto { sequence-a } else { reference } + let resolved-type = if seq-type == auto { alignment.at("seq-type") } else { _upper(seq-type) } + let total = 0 + let hits = 0 + for col in _selected-columns(alignment, resolved-reference, selection) { + let a = left.at("aligned").slice(col, col + 1) + let b = right.at("aligned").slice(col, col + 1) + if not _is-gap(a) and not _is-gap(b) { + total += 1 + if _upper(a) == _upper(b) { + hits += 1 + } else if kind == "similarity" and _similar-residue(a, b, resolved-type, similarities: similarities, groups: groups) { + hits += 1 + } + } + } + if total == 0 { + 0.0 + } else { + calc.round(hits / total * 1000) / 10 + } +} + +#let percent-identity( + source, + sequence-a, + sequence-b, + format: auto, + selection: "all", + reference: auto, + seq-type: auto, +) = { + let alignment = _parser.read-alignment(source, format: format) + _percent-score(alignment, sequence-a, sequence-b, "identity", selection: selection, reference: reference, seq-type: seq-type) +} + +#let percent-similarity( + source, + sequence-a, + sequence-b, + format: auto, + selection: "all", + reference: auto, + seq-type: auto, + similarities: none, + groups: none, +) = { + let alignment = _parser.read-alignment(source, format: format) + _percent-score( + alignment, + sequence-a, + sequence-b, + "similarity", + selection: selection, + reference: reference, + seq-type: seq-type, + similarities: similarities, + groups: groups, + ) +} + +#let similarity-table( + source, + format: auto, + selection: "all", + reference: auto, + seq-type: auto, + similarities: none, + groups: none, +) = { + let alignment = _parser.read-alignment(source, format: format) + let sequences = alignment.at("sequences") + let columns = (auto,) + let rows = ([],) + for seq in sequences { + columns.push(auto) + rows.push([#seq.at("name")]) + } + for (row-index, row-seq) in sequences.enumerate() { + rows.push([#row-seq.at("name")]) + for (col-index, col-seq) in sequences.enumerate() { + if row-index == col-index { + rows.push([-]) + } else { + let kind = if col-index > row-index { "similarity" } else { "identity" } + let value = _percent-score( + alignment, + row-index + 1, + col-index + 1, + kind, + selection: selection, + reference: if reference == auto { row-index + 1 } else { reference }, + seq-type: seq-type, + similarities: similarities, + groups: groups, + ) + rows.push([#str(value)]) + } + } + } + table(columns: columns, inset: (x: 5pt, y: 3pt), ..rows) +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/annotations.typ b/packages/preview/typshade/0.1.3/internal/interface/annotations.typ new file mode 100644 index 0000000000..8333bdaa5a --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/annotations.typ @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config + +#let pdb-point(source, residue, distance: 1, atom: "side") = ( + kind: "pdb-selection", + shape: "point", + source: source, + distance: distance, + anchors: ((residue: residue, atom: atom),), +) + +#let pdb-line(source, residue-a, residue-b, distance: 1, atom-a: "side", atom-b: "side") = ( + kind: "pdb-selection", + shape: "line", + source: source, + distance: distance, + anchors: ((residue: residue-a, atom: atom-a), (residue: residue-b, atom: atom-b)), +) + +#let pdb-plane(source, residue-a, residue-b, residue-c, distance: 1, atom-a: "side", atom-b: "side", atom-c: "side") = ( + kind: "pdb-selection", + shape: "plane", + source: source, + distance: distance, + anchors: ((residue: residue-a, atom: atom-a), (residue: residue-b, atom: atom-b), (residue: residue-c, atom: atom-c)), +) + +#let highlight(sequence, selection, fg: "White", bg: "RoyalBlue", all: false) = _config.region-highlight(sequence, selection, fg, bg, apply-to-all: all) +#let tint(sequence, selection, intensity: "medium") = _config.region-tint(sequence, selection, intensity: intensity) +#let emphasize(sequence, selection, style: "italic") = _config.region-emphasis(sequence, selection, style: style) + +#let mark(position, sequence, selection, fill: "Yellow", text: "", style: none) = { + let resolved-style = if style == none { "box[" + str(fill) + "]" } else { style } + _config.feature(position, sequence, selection, style: resolved-style, text: text) +} + +#let motif(sequence, selection, text: "motif", position: "top", fg: "White", bg: "RoyalBlue", fill: "Yellow", all: false) = ( + highlight(sequence, selection, fg: fg, bg: bg, all: all), + mark(position, sequence, selection, fill: fill, text: text), +) + +#let graph(position, sequence, selection, metric, kind: "bar", range: none, options: none, text: "") = { + let style = ( + kind: str(kind), + metric: metric, + min: if range == none { none } else { range.at(0) }, + max: if range == none { none } else { range.at(1) }, + options: if options == none { () } else { options }, + ) + _config.feature(position, sequence, selection, style: style, text: text) +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/controls.typ b/packages/preview/typshade/0.1.3/internal/interface/controls.typ new file mode 100644 index 0000000000..c956dd8f23 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/controls.typ @@ -0,0 +1,191 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config + +#let sequence-type(value) = _config.sequence-type(value) +#let color-scheme(name) = _config.color-scheme(name) +#let scoring-mode(name, option: none) = _config.scoring-mode(name, option: option) +#let tcoffee-scores(source) = scoring-mode("T-Coffee", option: source) +#let sequence-window(sequence, selection, start: none) = _config.sequence-window(sequence, selection, start: start) +#let residues-per-line(value) = _config.residues-per-line(value) +#let auto-layout(fit: "container", min: 1, max: none) = _config.auto-layout(fit: fit, min: min, max: max) +#let auto-page(blocks: auto, repeat-legend: true) = _config.auto-page(blocks: blocks, repeat-legend: repeat-legend) +#let threshold(value) = _config.threshold(value) +#let shade-all-residues() = _config.shade-all-residues() +#let all-match-threshold(value: 100) = _config.all-match-threshold(value: value) +#let disable-all-match-threshold() = _config.disable-all-match-threshold() +#let hide-all-match-positions() = _config.hide-all-match-positions() +#let show-all-match-positions() = _config.show-all-match-positions() + +#let weight-table(name) = _config.weight-table(name) +#let set-weight(residue-a, residue-b, value) = _config.set-weight(residue-a, residue-b, value) +#let gap-penalty(value) = _config.gap-penalty(value) + +#let residue-style(target, fg, bg, case: "upper", style: "normal") = _config.residue-style(target, fg, bg, case: case, style: style) +#let cell-style(callback) = _config.cell-style(callback) +#let peptide-groups(groups) = _config.peptide-groups(groups) +#let dna-groups(groups) = _config.dna-groups(groups) +#let peptide-similarities(residue, similars) = _config.peptide-similarities(residue, similars) +#let dna-similarities(residue, similars) = _config.dna-similarities(residue, similars) +#let clear-functional-groups() = _config.clear-functional-groups() +#let functional-group(name, residues, fg, bg, case: "upper", style: "normal") = _config.functional-group(name, residues, fg, bg, case: case, style: style) +#let functional-style(residue, fg, bg, case: "upper", style: "normal") = _config.functional-style(residue, fg, bg, case: case, style: style) + +#let names-track(position: "left", color: none) = _config.names-track(position: position, color: color) +#let no-names() = _config.no-names-track() +#let numbering-track(position: "right", color: none) = _config.numbering-track(position: position, color: color) +#let no-numbering() = _config.no-numbering-track() +#let sequence-name(sequence, name) = _config.sequence-name(sequence, name) +#let names-color(color) = _config.names-color(color) +#let sequence-name-color(sequences, color) = _config.sequence-name-color(sequences, color) +#let hide-sequence-name(sequences) = _config.hide-sequence-name(sequences) +#let numbering-color(color) = _config.numbering-color(color) +#let sequence-number-color(sequences, color) = _config.sequence-number-color(sequences, color) +#let hide-sequence-number(sequences) = _config.hide-sequence-number(sequences) + +#let consensus-name(name) = _config.consensus-name(name) +#let consensus-language(name) = _config.language(name) +#let consensus-symbols(none-symbol, conserved-symbol, allmatch-symbol) = _config.consensus-symbols(none-symbol, conserved-symbol, allmatch-symbol) +#let consensus-colors(none-fg: "Black", none-bg: "White", conserved-fg: "Black", conserved-bg: "White", allmatch-fg: "Black", allmatch-bg: "White") = _config.consensus-colors( + none-fg: none-fg, + none-bg: none-bg, + conserved-fg: conserved-fg, + conserved-bg: conserved-bg, + allmatch-fg: allmatch-fg, + allmatch-bg: allmatch-bg, +) +#let consensus-from-sequence(sequence) = _config.consensus-from-sequence(sequence) +#let consensus-from-all-sequences() = _config.consensus-from-all-sequences() + +#let ruler-steps(value, position: none) = _config.ruler-steps(value, position: position) +#let ruler-color(color, position: none) = _config.ruler-color(color, position: position) +#let ruler-name(name, position: none) = _config.ruler-name(name, position: position) +#let ruler-name-color(color, position: none) = _config.ruler-name-color(color, position: position) +#let ruler-space(value, position: none) = _config.ruler-space(value, position: position) +#let rotate-ruler(position: none) = _config.rotate-ruler(position) +#let unrotate-ruler(position: none) = _config.unrotate-ruler(position) + +#let gap-char(symbol) = _config.gap-char(symbol) +#let gap-rule(thickness) = _config.gap-rule(thickness) +#let gap-colors(foreground, background) = _config.gap-colors(foreground, background) +#let stop-char(symbol) = _config.stop-char(symbol) +#let show-leading-gaps() = _config.show-leading-gaps() +#let hide-leading-gaps() = _config.hide-leading-gaps() + +#let start-number(sequence, start, selection: none) = _config.start-number(sequence, start, selection: selection) +#let allow-zero-numbering() = _config.allow-zero-numbering() +#let disallow-zero-numbering() = _config.disallow-zero-numbering() +#let sequence-length(sequence, length) = _config.sequence-length(sequence, length) +#let domain(sequence, selection) = _config.domain(sequence, selection) +#let domain-gap-rule(thickness) = _config.domain-gap-rule(thickness) +#let domain-gap-colors(foreground, background) = _config.domain-gap-colors(foreground, background) + +#let highlight-block(sequence, selection, ..args) = _config.highlight-block(sequence, selection, ..args) +#let region-color-scheme(sequence, selection, scheme) = _config.region-color-scheme(sequence, selection, scheme) +#let lower(sequence, selection) = _config.region-lower(sequence, selection) +#let lower-block(sequence, selection) = _config.lower-block(sequence, selection) +#let emphasis-block(sequence, selection, style: "italic") = _config.emphasis-block(sequence, selection, style: style) +#let tint-block(sequence, selection, intensity: "medium") = _config.tint-block(sequence, selection, intensity: intensity) +#let tint-default(effect) = _config.tint-default(effect) +#let emphasis-default(style) = _config.emphasis-default(style) +#let frame(sequence, selection, color: "Red") = _config.frame-block(sequence, selection, color: color) + +#let hide-sequence(sequence) = _config.hide-sequence(sequence) +#let hide-all-sequences() = _config.hide-all-sequences() +#let show-all-sequences() = _config.show-all-sequences() +#let remove-sequence(sequence) = _config.remove-sequence(sequence) +#let no-shade(sequences) = _config.no-shade(sequences) +#let separation-line(sequence) = _config.separation-line(sequence) +#let sequence-order(order) = _config.sequence-order(order) + +#let feature-rule(thickness) = _config.feature-rule(thickness) +#let codon(residue, triplets) = _config.codon(residue, triplets) +#let genetic-code(name) = _config.genetic-code(name) +#let backtranslation-label(..args) = _config.backtranslation-label(..args) +#let backtranslation-text(..args) = _config.backtranslation-text(..args) +#let feature-text-label(position, name) = _config.feature-text-label(position, name) +#let feature-style-label(position, name) = _config.feature-style-label(position, name) +#let hide-feature-text-label(position) = _config.hide-feature-text-label(position) +#let hide-feature-style-label(position) = _config.hide-feature-style-label(position) +#let hide-feature-text-labels() = _config.hide-feature-text-labels() +#let hide-feature-style-labels() = _config.hide-feature-style-labels() +#let feature-text-label-color(color) = _config.feature-text-label-color(color) +#let feature-style-label-color(color) = _config.feature-style-label-color(color) +#let feature-text-label-color-at(position, color) = _config.feature-text-label-color-at(position, color) +#let feature-style-label-color-at(position, color) = _config.feature-style-label-color-at(position, color) + +#let frequency-correction() = _config.frequency-correction() +#let no-frequency-correction() = _config.no-frequency-correction() +#let subfamily(sequences) = _config.subfamily(sequences) +#let sequence-logo-name(name) = _config.sequence-logo-name(name) +#let subfamily-logo-name(name, negative-name: none) = _config.subfamily-logo-name(name, negative-name: negative-name) +#let logo-scale(position: "leftright", color: "Black") = _config.logo-scale(position: position, color: color) +#let no-logo-scale() = _config.no-logo-scale() +#let logo-stretch(value) = _config.logo-stretch(value) +#let negative-logo-values() = _config.negative-logo-values() +#let no-negative-logo-values() = _config.no-negative-logo-values() +#let relevance-threshold(value) = _config.relevance-threshold(value) +#let relevance-marker(char: "*", color: "Black") = _config.relevance-marker(char: char, color: color) +#let no-relevance-marker() = _config.no-relevance-marker() +#let logo-color(residues, color) = _config.logo-color(residues, color) +#let clear-logo-colors(default: "Black") = _config.clear-logo-colors(default: default) + +#let no-legend() = _config.no-legend-track() +#let legend-color(color) = _config.legend-color(color) +#let legend-offset(dx, dy) = _config.legend-offset(dx, dy) +#let color-swatch(color) = _config.color-swatch(color) + +#let show-structure-types(format, types) = _config.show-structure-types(format, types) +#let hide-structure-types(format, types) = _config.hide-structure-types(format, types) +#let structure-appearance(format, structure-type, position, style, text) = _config.structure-appearance(format, structure-type, position, style, text) +#let use-first-dssp-column() = _config.use-first-dssp-column() +#let use-second-dssp-column() = _config.use-second-dssp-column() +#let stride-track(sequence, source) = _config.stride-track(sequence, source) +#let dssp-track(sequence, source) = _config.dssp-track(sequence, source) +#let hmmtop-track(sequence, source, source-sequence: none) = _config.hmmtop-track(sequence, source, source-sequence: source-sequence) +#let phd-topology-track(sequence, source) = _config.phd-topology-track(sequence, source) +#let phd-secondary-track(sequence, source) = _config.phd-secondary-track(sequence, source) + +#let keep-single-sequence-gaps() = _config.keep-single-sequence-gaps() +#let shift-single-sequence(value: -1) = _config.shift-single-sequence(value) +#let hide-residues() = _config.hide-residues() +#let show-residues() = _config.show-residues() +#let bar-graph-stretch(value, position: none) = _config.bar-graph-stretch(value, position: position) +#let color-scale-stretch(value, position: none) = _config.color-scale-stretch(value, position: position) +#let alignment-position(position) = _config.alignment(position) +#let character-stretch(value) = _config.character-stretch(value) +#let line-stretch(value) = _config.line-stretch(value) +#let numbering-width(digits) = _config.numbering-width(digits) +#let fingerprint(value) = _config.fingerprint(value) +#let align-right-labels() = _config.align-right-labels() +#let align-left-labels() = _config.align-left-labels() + +#let text-family(target, family) = _config.text-family(target, family) +#let text-weight(target, weight) = _config.text-weight(target, weight) +#let text-posture(target, posture) = _config.text-posture(target, posture) +#let text-size(target, size) = _config.text-size(target, size) +#let text-style(target, family, weight, posture, size) = _config.text-style(target, family, weight, posture, size) + +#let caption(text, position: "bottom") = _config.caption(text, position: position) +#let short-caption(text) = _config.short-caption(text) +#let small-separator() = _config.small-separator() +#let medium-separator() = _config.medium-separator() +#let large-separator() = _config.large-separator() +#let no-block-gap() = _config.no-block-gap() +#let small-block-gap() = _config.small-block-gap() +#let medium-block-gap() = _config.medium-block-gap() +#let large-block-gap() = _config.large-block-gap() +#let block-gap(value) = _config.block-gap(value) +#let flexible-block-gap() = _config.flexible-block-gap() +#let fixed-block-gap() = _config.fixed-block-gap() +#let no-line-gap() = _config.no-line-gap() +#let small-line-gap() = _config.small-line-gap() +#let medium-line-gap() = _config.medium-line-gap() +#let large-line-gap() = _config.large-line-gap() +#let line-gap(value) = _config.line-gap(value) +#let feature-slot-space(position, value) = _config.feature-slot-space(position, value) + +#let molecular-weight(sequence, unit: "Da") = _config.molweight(sequence, unit: unit) +#let net-charge(sequence, termini: "o") = _config.charge(sequence, termini: termini) +#let pdb-selection(selection) = _config.pdb-selection(selection) diff --git a/packages/preview/typshade/0.1.3/internal/interface/data.typ b/packages/preview/typshade/0.1.3/internal/interface/data.typ new file mode 100644 index 0000000000..c9bb2fae94 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/data.typ @@ -0,0 +1,18 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../model/parser.typ" as _parser + +#let alignment-data(source, format: auto) = _parser.read-alignment(source, format: format) + +#let parse-alignment(text, format: "fasta") = { + let source = _parser._source-text(text) + let normalized = _parser._lower(str(format)) + if normalized == "msf" { + _parser.parse-msf(source) + } else if normalized == "aln" or normalized == "clustal" { + _parser.parse-aln(source) + } else { + _parser.parse-fasta(source) + } +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/inspect.typ b/packages/preview/typshade/0.1.3/internal/interface/inspect.typ new file mode 100644 index 0000000000..09d61c932b --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/inspect.typ @@ -0,0 +1,151 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/layout.typ" as _layout +#import "../engine/commands.typ" as _commands +#import "../engine/config.typ" as _config +#import "../model/logo.typ" as _logo +#import "../model/parser.typ" as _parser +#import "../render/alignment.typ" as _render + +#let _prepared-alignment(source, format, commands) = { + let config = _config._default-config() + let flat = () + flat = _commands._add-command(flat, commands) + for command in flat { + config = _config._apply-command(config, command) + } + let alignment = _parser.read-alignment(source, format: format) + if config.at("seq-type") != none { + alignment.insert("seq-type", config.at("seq-type")) + } + let _ = _render._validate-config-sequence-refs(alignment, config) + _render._apply-single-sequence-options(alignment, config) + _render._apply-numbering-overrides(alignment, config) + (alignment: alignment, config: config) +} + +#let _debug-value(value) = if value == auto { "auto" } else { str(value) } + +#let _selection-label(selection) = if type(selection) == dictionary and selection.at("kind", default: none) == "typshade-selection" { + selection.at("op", default: "select") +} else { + str(selection) +} + +#let alignment-summary(source, format: auto) = { + let alignment = _parser.read-alignment(source, format: format) + let names = alignment.at("sequences").map(seq => seq.at("name")).join(", ") + table( + columns: (auto, 1fr), + inset: (x: 6pt, y: 3pt), + stroke: (x, y) => if y == 0 { (bottom: 0.6pt) } else { none }, + [Format], [#alignment.at("format")], + [Type], [#alignment.at("seq-type")], + [Sequences], [#str(alignment.at("sequences").len())], + [Columns], [#str(alignment.at("columns"))], + [Names], [#names], + ) +} + +#let selection-preview(source, sequence, selection, format: auto) = { + let alignment = _parser.read-alignment(source, format: format) + let resolved-sequence = alignment.at("sequences").at(_logo._resolve-sequence(alignment, sequence)) + let positions = () + for col in _layout._selection-columns(resolved-sequence, selection, alignment: alignment) { + let pos = resolved-sequence.at("positions").at(col) + if pos != none { + positions.push(str(pos)) + } + } + if positions.len() == 0 { "" } else { positions.join(",") } +} + +#let sequence-list(source, format: auto) = { + let alignment = _parser.read-alignment(source, format: format) + let rows = ([Name], [Length], [Non-gap residues]) + for sequence in alignment.at("sequences") { + let count = sequence.at("positions").filter(pos => pos != none).len() + rows.push([#sequence.at("name")]) + rows.push([#str(sequence.at("aligned").len())]) + rows.push([#str(count)]) + } + table(columns: (1fr, auto, auto), inset: (x: 5pt, y: 3pt), ..rows) +} + +#let selection-table(source, ..items, format: auto, sequence: 1) = { + let rows = ([Name], [Selection], [Positions], [Count]) + for item in items.pos() { + let selection = if type(item) == dictionary { item.at("selection") } else { item } + let name = if type(item) == dictionary and item.keys().contains("name") { item.at("name") } else { _selection-label(selection) } + let item-sequence = if type(item) == dictionary { item.at("sequence", default: sequence) } else { sequence } + let positions = selection-preview(source, item-sequence, selection, format: format) + let count = if positions == "" { 0 } else { positions.split(",").len() } + rows.push([#name]) + rows.push([#_selection-label(selection)]) + rows.push([#positions]) + rows.push([#str(count)]) + } + table(columns: (auto, auto, 1fr, auto), inset: (x: 5pt, y: 3pt), ..rows) +} + +#let alignment-debug(source, format: auto, commands: ()) = { + let prepared = _prepared-alignment(source, format, commands) + let alignment = prepared.at("alignment") + let config = prepared.at("config") + let visible = _render._visible-sequences(alignment, config).map(seq => seq.at("name")).join(", ") + let display-columns = _render._display-columns(alignment, config) + table( + columns: (auto, 1fr), + inset: (x: 6pt, y: 3pt), + stroke: (x, y) => if y == 0 { (bottom: 0.6pt) } else { none }, + [Field], [Value], + [Format], [#alignment.at("format")], + [Type], [#alignment.at("seq-type")], + [Sequences], [#str(alignment.at("sequences").len())], + [Visible sequences], [#visible], + [Columns], [#str(alignment.at("columns"))], + [Displayed columns], [#str(display-columns.len())], + [Residues per line], [#_debug-value(config.at("residues-per-line"))], + [Scoring mode], [#str(config.at("shading").at("mode"))], + [Consensus source], [#str(config.at("consensus").at("source", default: "all"))], + ) +} + +#let cell-inspect(source, sequence, column, format: auto, commands: ()) = { + let prepared = _prepared-alignment(source, format, commands) + let alignment = prepared.at("alignment") + let config = prepared.at("config") + let seq-index = _logo._resolve-sequence(alignment, sequence) + let sequence-data = alignment.at("sequences").at(seq-index) + let col = int(column) - 1 + assert(col >= 0 and col < alignment.at("columns"), message: "typshade: column `" + str(column) + "` is out of range") + let info = _render._style-for-column(alignment, config, col) + let style-info = info.at("styles").at(seq-index) + let cell = ( + char: style-info.at("char"), + fg: style-info.at("fg"), + bg: style-info.at("bg"), + emph: style-info.at("emph", default: false), + frame: none, + rule: style-info.at("rule", default: false), + ) + cell = _render._apply-cell-styles(alignment, config, sequence-data, seq-index, col, info, style-info, cell) + cell = _render._apply-regions(alignment, config, seq-index, col, cell) + table( + columns: (auto, 1fr), + inset: (x: 6pt, y: 3pt), + stroke: (x, y) => if y == 0 { (bottom: 0.6pt) } else { none }, + [Field], [Value], + [Sequence], [#sequence-data.at("name")], + [Column], [#str(column)], + [Position], [#str(sequence-data.at("positions").at(col))], + [Residue], [#sequence-data.at("aligned").slice(col, col + 1)], + [Kind], [#style-info.at("kind", default: "custom")], + [Rendered char], [#cell.at("char")], + [Foreground], [#str(cell.at("fg"))], + [Background], [#str(cell.at("bg"))], + [Consensus], [#str(info.at("consensus"))], + [Consensus score], [#str(calc.round(info.at("consensus-score") * 10) / 10)], + ) +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/presets.typ b/packages/preview/typshade/0.1.3/internal/interface/presets.typ new file mode 100644 index 0000000000..15712af8c5 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/presets.typ @@ -0,0 +1,140 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config +#import "../engine/commands.typ" as _commands + +#let visual-theme( + colors: none, + mode: none, + option: none, + gap: none, + names: none, + numbering: none, + ruler: none, + legend: none, + commands: (), +) = ( + colors: colors, + mode: mode, + option: option, + gap: gap, + names: names, + numbering: numbering, + ruler: ruler, + legend: legend, + commands: commands, +) + +#let _theme-commands(settings) = { + let out = () + if settings.at("mode", default: none) != none { + out.push(_config.scoring-mode(settings.at("mode"), option: settings.at("option", default: none))) + } + if settings.at("colors", default: none) != none { + out.push(_config.color-scheme(settings.at("colors"))) + } + let gap = settings.at("gap", default: none) + if type(gap) == dictionary { + if gap.at("foreground", default: none) != none or gap.at("background", default: none) != none { + out.push(_config.gap-colors(gap.at("foreground", default: "Black"), gap.at("background", default: "White"))) + } + if gap.at("rule", default: none) != none { + out.push(_config.gap-rule(gap.at("rule"))) + } + } + if settings.at("names", default: none) != none { + out.push(_config.names-color(settings.at("names"))) + } + if settings.at("numbering", default: none) != none { + out.push(_config.numbering-color(settings.at("numbering"))) + } + if settings.at("ruler", default: none) != none { + out.push(_config.ruler-color(settings.at("ruler"))) + } + if settings.at("legend", default: none) != none { + out.push(_config.legend-color(settings.at("legend"))) + } + out = _commands._add-command(out, settings.at("commands", default: ())) + out +} + +#let shade-theme(name) = { + if name == none or name == "classic" { + () + } else if type(name) == dictionary { + _theme-commands(name) + } else if name == "print" or name == "grayscale" { + ( + _config.color-scheme("grays"), + _config.gap-colors("Gray60", "White"), + _config.names-color("Black"), + _config.numbering-color("Gray60"), + ) + } else if name == "screen" or name == "blue" { + ( + _config.color-scheme("blues"), + _config.names-color("RoyalBlue"), + _config.numbering-color("DarkGray"), + ) + } else if name == "warm" { + ( + _config.color-scheme("reds"), + _config.names-color("BrickRed"), + _config.numbering-color("DarkGray"), + ) + } else if name == "nature" or name == "green" { + ( + _config.color-scheme("greens"), + _config.names-color("PineGreen"), + _config.numbering-color("DarkGray"), + ) + } else if type(name) == array { + name + } else { + () + } +} + +#let shade-preset(name) = { + if name == none { + () + } else if name == "publication" { + ( + shade-theme("print"), + _config.names-track(position: "left"), + _config.numbering-track(position: "right"), + _config.consensus-track(position: "bottom"), + _config.residues-per-line(60), + ) + } else if name == "overview" { + ( + _config.fingerprint(1000), + _config.no-numbering-track(), + _config.no-consensus-track(), + _config.align-left-labels(), + ) + } else if name == "logo" { + ( + _config.sequence-logo-track(position: "top"), + _config.logo-scale(position: "leftright"), + _config.consensus-track(position: "bottom"), + ) + } else if name == "functional" { + ( + _config.scoring-mode("functional", option: "charge"), + _config.legend-track(), + ) + } else if name == "structure" { + ( + _config.numbering-track(position: "leftright"), + _config.ruler-track(position: "top", steps: 10), + _config.ruler-color("DarkGray"), + _config.consensus-track(position: "bottom"), + ) + } else if type(name) == array { + name + } else { + () + } +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/recipes.typ b/packages/preview/typshade/0.1.3/internal/interface/recipes.typ new file mode 100644 index 0000000000..54eb1bcecd --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/recipes.typ @@ -0,0 +1,726 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config +#import "../engine/commands.typ" as _commands +#import "../engine/layout.typ": _selection-columns +#import "../model/logo.typ": _resolve-sequence +#import "../model/parser.typ": read-alignment +#import "annotations.typ" as _annotations +#import "tracks.typ": sequence-logo, structure-tracks +#import "presets.typ": shade-preset, shade-theme + +#let _recipe(name, options) = (kind: "typshade-recipe", name: name, options: options) + +#let _is-recipe(value) = type(value) == dictionary and value.at("kind", default: none) == "typshade-recipe" + +#let _add(out, value) = _commands._add-command(out, value) + +#let _value(value, key, default) = if type(value) == dictionary { + value.at(key, default: default) +} else { + default +} + +#let _track-position(value, default) = if value == true { + default +} else if type(value) == dictionary { + value.at("position", default: default) +} else { + value +} + +#let _sequence-index(alignment, sequence) = { + _resolve-sequence(alignment, sequence) +} + +#let _sequence(alignment, sequence) = alignment.at("sequences").at(_sequence-index(alignment, sequence)) + +#let _clamp(value, low, high) = { + if value < low { + low + } else if value > high { + high + } else { + value + } +} + +#let _selection-span(alignment, sequence, selection, padding: 0) = { + let seq = _sequence(alignment, sequence) + let columns = _selection-columns(seq, selection, alignment: alignment) + let first = none + let last = none + for col in columns { + let pos = seq.at("positions").at(col) + if pos != none { + if first == none or pos < first { + first = pos + } + if last == none or pos > last { + last = pos + } + } + } + if first == none or last == none { + none + } else { + let start = _clamp(first - padding, 1, seq.at("length")) + let stop = _clamp(last + padding, 1, seq.at("length")) + str(start) + ".." + str(stop) + } +} + +#let _motif-patterns(motifs) = { + if motifs == none { + return () + } + if type(motifs) == dictionary { + motifs.keys() + } else if type(motifs) == array { + let out = () + for item in motifs { + if type(item) == str { + out.push(item) + } else if type(item) == dictionary { + out.push(_annotation-selection(item, "all")) + } + } + out + } else { + () + } +} + +#let _auto-region(alignment, sequence, motifs, padding: 8) = { + let seq = _sequence(alignment, sequence) + let first = none + let last = none + for pattern in _motif-patterns(motifs) { + let span = _selection-span(alignment, sequence, pattern) + if span != none { + let bounds = span.split("..") + let start = int(bounds.first()) + let stop = int(bounds.last()) + if first == none or start < first { + first = start + } + if last == none or stop > last { + last = stop + } + } + } + if first == none or last == none { + none + } else { + str(_clamp(first - padding, 1, seq.at("length"))) + ".." + str(_clamp(last + padding, 1, seq.at("length"))) + } +} + +#let _has-selection(alignment, sequence, selection) = _selection-span(alignment, sequence, selection) != none + +#let _auto-motifs(alignment, sequence) = { + let candidates = if alignment.at("seq-type") == "N" { + ( + (pattern: "ATG", text: "start"), + (pattern: "TATA", text: "TATA box"), + ) + } else { + ( + (pattern: "NPA", text: "NPA"), + (pattern: "NXX[ST]N", text: "glycosylation"), + (pattern: "CXXC", text: "CXXC"), + (pattern: "HXXH", text: "HXXH"), + ) + } + let out = (:) + for item in candidates { + if _has-selection(alignment, sequence, item.at("pattern")) { + out.insert(item.at("pattern"), item.at("text")) + } + } + out +} + +#let _auto-line-length(alignment, purpose: "publication", region: none, sequence: 1) = { + if region != none and region != auto { + let span = _selection-span(alignment, sequence, region) + if span != none { + let bounds = span.split("..") + return _clamp(int(bounds.last()) - int(bounds.first()) + 1, 35, 90) + } + } + let columns = alignment.at("columns") + let count = alignment.at("sequences").len() + if purpose == "overview" { + if columns <= 120 { columns } else { 120 } + } else if columns <= 55 { + columns + } else if count >= 20 { + 80 + } else if count >= 10 { + 70 + } else { + 60 + } +} + +#let _auto-threshold(alignment, threshold) = { + if threshold != auto { + return threshold + } + if alignment.at("seq-type") == "N" { + 60 + } else { + 45 + } +} + +#let _auto-logo(alignment, logo, region: none) = { + if logo != auto { + return logo + } + let count = alignment.at("sequences").len() + let columns = alignment.at("columns") + let compact = if region == none or region == auto { + columns <= 140 + } else { + true + } + if count >= 4 and compact { + if alignment.at("seq-type") == "N" { "nucleotide" } else { "charge" } + } else { + false + } +} + +#let _auto-conservation(alignment, value) = { + if value == auto { + alignment.at("sequences").len() >= 2 + } else { + value + } +} + +#let _apply-scoring(out, mode, colors, threshold, option: none) = { + if mode != none { + out.push(_config.scoring-mode(mode, option: option)) + } + if colors != none { + out.push(_config.color-scheme(colors)) + } + if threshold != none { + out.push(_config.threshold(threshold)) + } + out +} + +#let _apply-window(out, region, sequence, start: none) = { + if region != none { + if type(region) == dictionary { + out.push(_config.sequence-window( + region.at("sequence", default: sequence), + region.at("selection", default: region.at("range", default: "all")), + start: region.at("start", default: start), + )) + } else { + out.push(_config.sequence-window(sequence, region, start: start)) + } + } + out +} + +#let _apply-ruler(out, value, sequence, every) = { + if value == false or value == none { + return out + } + let position = _track-position(value, "top") + let steps = _value(value, "every", _value(value, "steps", every)) + out.push(_config.ruler-track( + position: position, + sequence: _value(value, "sequence", sequence), + steps: steps, + color: _value(value, "color", none), + )) + if _value(value, "name", none) != none { + out.push(_config.ruler-name(_value(value, "name", none), position: position)) + } + out +} + +#let _apply-consensus(out, value) = { + if value == false or value == none { + return out + } + out.push(_config.consensus-track( + position: _track-position(value, "bottom"), + scale: _value(value, "scale", none), + name: _value(value, "name", none), + )) + out +} + +#let _apply-logo(out, value) = { + if value == false or value == none { + return out + } + if value == true or type(value) == str { + out = _add(out, sequence-logo(position: "top", colors: if value == true { none } else { value })) + } else if type(value) == dictionary { + out = _add(out, sequence-logo( + position: value.at("position", default: "top"), + colors: value.at("colors", default: value.at("colorset", default: none)), + name: value.at("name", default: none), + scale: value.at("scale", default: "leftright"), + relevance-marker: value.at("relevance-marker", default: none), + stretch: value.at("stretch", default: none), + )) + } + out +} + +#let _annotation-selection(item, default) = item.at("selection", default: item.at("range", default: item.at("pattern", default: default))) + +#let _add-motif(out, pattern, settings, sequence, position) = { + if type(settings) == str { + out = _add(out, _annotations.motif(sequence, pattern, text: settings, position: position)) + } else if type(settings) == dictionary { + out = _add(out, _annotations.motif( + settings.at("sequence", default: sequence), + pattern, + text: settings.at("text", default: settings.at("label", default: "motif")), + position: settings.at("position", default: position), + fg: settings.at("fg", default: "White"), + bg: settings.at("bg", default: "RoyalBlue"), + fill: settings.at("fill", default: "Yellow"), + all: settings.at("all", default: false), + )) + } else if settings == true { + out = _add(out, _annotations.motif(sequence, pattern, position: position)) + } + out +} + +#let _apply-motifs(out, motifs, sequence, position: "top") = { + if motifs == none { + return out + } + if type(motifs) == dictionary { + for pattern in motifs.keys() { + out = _add-motif(out, pattern, motifs.at(pattern), sequence, position) + } + } else if type(motifs) == array { + for item in motifs { + if type(item) == str { + out = _add(out, _annotations.motif(sequence, item, position: position)) + } else if type(item) == dictionary { + out = _add-motif( + out, + _annotation-selection(item, "all"), + item, + item.at("sequence", default: sequence), + item.at("position", default: position), + ) + } else { + out = _add(out, item) + } + } + } + out +} + +#let _add-highlight(out, selection, settings, sequence) = { + if type(settings) == dictionary { + out = _add(out, _annotations.highlight( + settings.at("sequence", default: sequence), + selection, + fg: settings.at("fg", default: "White"), + bg: settings.at("bg", default: settings.at("fill", default: "RoyalBlue")), + all: settings.at("all", default: false), + )) + } else if settings == true { + out = _add(out, _annotations.highlight(sequence, selection)) + } else if type(settings) == str { + out = _add(out, _annotations.highlight(sequence, selection, bg: settings)) + } + out +} + +#let _apply-highlights(out, highlights, sequence) = { + if highlights == none { + return out + } + if type(highlights) == dictionary { + for selection in highlights.keys() { + out = _add-highlight(out, selection, highlights.at(selection), sequence) + } + } else if type(highlights) == array { + for item in highlights { + if type(item) == str { + out = _add(out, _annotations.highlight(sequence, item)) + } else if type(item) == dictionary { + out = _add-highlight(out, _annotation-selection(item, "all"), item, sequence) + } else { + out = _add(out, item) + } + } + } + out +} + +#let publication( + mode: "similar", + similarity: "blues", + threshold: auto, + sequence: 1, + region: auto, + line-length: auto, + ruler: true, + every: 10, + conservation: true, + logo: none, + motifs: (:), + highlights: (), + theme: none, + annotations: (), + commands: (), +) = _recipe("publication", ( + mode: mode, + similarity: similarity, + threshold: threshold, + sequence: sequence, + region: region, + line-length: line-length, + ruler: ruler, + every: every, + conservation: conservation, + logo: logo, + motifs: motifs, + highlights: highlights, + theme: theme, + annotations: annotations, + commands: commands, +)) + +#let _build-publication(alignment, options) = { + let mode = options.at("mode") + let similarity = options.at("similarity") + let threshold = _auto-threshold(alignment, options.at("threshold")) + let sequence = options.at("sequence") + let motifs = if options.at("motifs") == auto { _auto-motifs(alignment, sequence) } else { options.at("motifs") } + let region = if options.at("region") == auto { _auto-region(alignment, sequence, motifs) } else { options.at("region") } + let line-length = if options.at("line-length") == auto { + _auto-line-length(alignment, purpose: "publication", region: region, sequence: sequence) + } else { + options.at("line-length") + } + let conservation = _auto-conservation(alignment, options.at("conservation")) + let logo = _auto-logo(alignment, options.at("logo"), region: region) + let out = () + out = _add(out, shade-preset("publication")) + out = _add(out, shade-theme(options.at("theme"))) + out = _apply-scoring(out, mode, similarity, threshold) + if line-length != none { + out.push(_config.residues-per-line(line-length)) + } + out = _apply-window(out, region, sequence) + out = _apply-ruler(out, options.at("ruler"), sequence, options.at("every")) + out = _apply-consensus(out, conservation) + out = _apply-logo(out, logo) + out = _apply-highlights(out, options.at("highlights"), sequence) + out = _apply-motifs(out, motifs, sequence) + out = _add(out, options.at("annotations")) + out = _add(out, options.at("commands")) + out +} + +#let motif-map( + motifs, + sequence: 1, + region: auto, + line-length: auto, + similarity: "blues", + threshold: auto, + logo: auto, + conservation: auto, + graph: auto, + theme: none, + highlights: (), + commands: (), +) = _recipe("motif-map", ( + motifs: motifs, + sequence: sequence, + region: region, + line-length: line-length, + similarity: similarity, + threshold: threshold, + logo: logo, + conservation: conservation, + graph: graph, + theme: theme, + highlights: highlights, + commands: commands, +)) + +#let _build-motif-map(alignment, options) = { + let sequence = options.at("sequence") + let motifs = if options.at("motifs") == auto { _auto-motifs(alignment, sequence) } else { options.at("motifs") } + let region = if options.at("region") == auto { _auto-region(alignment, sequence, motifs) } else { options.at("region") } + let out = _build-publication(alignment, ( + mode: "similar", + similarity: options.at("similarity"), + threshold: options.at("threshold"), + sequence: sequence, + region: region, + line-length: options.at("line-length"), + ruler: true, + every: 10, + conservation: options.at("conservation"), + logo: options.at("logo"), + motifs: motifs, + highlights: options.at("highlights"), + theme: options.at("theme"), + annotations: (), + commands: (), + )) + let graph = if options.at("graph") == auto { alignment.at("sequences").len() >= 3 } else { options.at("graph") } + if graph != false { + if type(graph) == dictionary { + out = _add(out, _annotations.graph( + graph.at("position", default: "bottom"), + graph.at("sequence", default: sequence), + graph.at("selection", default: graph.at("range", default: "all")), + graph.at("metric", default: "conservation"), + kind: graph.at("kind", default: "color"), + options: graph.at("options", default: ("ColdHot",)), + )) + } else { + out = _add(out, _annotations.graph("bottom", sequence, "all", "conservation", kind: "color", options: ("ColdHot",))) + } + } + out = _add(out, options.at("commands")) + out +} + +#let structure-map( + sequence, + topology: none, + secondary: none, + hmmtop: none, + hmmtop-sequence: none, + region: none, + line-length: auto, + similarity: "grays", + threshold: auto, + ruler: true, + conservation: auto, + theme: none, + commands: (), +) = _recipe("structure-map", ( + sequence: sequence, + topology: topology, + secondary: secondary, + hmmtop: hmmtop, + hmmtop-sequence: hmmtop-sequence, + region: region, + line-length: line-length, + similarity: similarity, + threshold: threshold, + ruler: ruler, + conservation: conservation, + theme: theme, + commands: commands, +)) + +#let _build-structure-map(alignment, options) = { + let sequence = options.at("sequence") + let region = options.at("region") + let line-length = if options.at("line-length") == auto { + _auto-line-length(alignment, purpose: "structure", region: region, sequence: sequence) + } else { + options.at("line-length") + } + let threshold = if options.at("threshold") == auto { none } else { options.at("threshold") } + let conservation = _auto-conservation(alignment, options.at("conservation")) + let out = () + out = _add(out, shade-preset("structure")) + out = _add(out, shade-theme(options.at("theme"))) + out = _apply-scoring(out, "similar", options.at("similarity"), threshold) + if line-length != none { + out.push(_config.residues-per-line(line-length)) + } + out = _apply-window(out, region, sequence) + out = _apply-ruler(out, options.at("ruler"), sequence, 10) + out = _apply-consensus(out, conservation) + out = _add(out, structure-tracks( + sequence, + hmmtop: options.at("hmmtop"), + topology: options.at("topology"), + secondary: options.at("secondary"), + hmmtop-sequence: options.at("hmmtop-sequence"), + )) + out = _add(out, options.at("commands")) + out +} + +#let logo-analysis( + sequence: 1, + region: none, + line-length: auto, + colors: auto, + subfamily: none, + negative: auto, + relevance: auto, + conservation: auto, + theme: none, + commands: (), +) = _recipe("logo-analysis", ( + sequence: sequence, + region: region, + line-length: line-length, + colors: colors, + subfamily: subfamily, + negative: negative, + relevance: relevance, + conservation: conservation, + theme: theme, + commands: commands, +)) + +#let _build-logo-analysis(alignment, options) = { + let sequence = options.at("sequence") + let region = options.at("region") + let line-length = if options.at("line-length") == auto { + _auto-line-length(alignment, purpose: "logo", region: region, sequence: sequence) + } else { + options.at("line-length") + } + let colors = if options.at("colors") == auto { + if alignment.at("seq-type") == "N" { "nucleotide" } else { "charge" } + } else { + options.at("colors") + } + let conservation = _auto-conservation(alignment, options.at("conservation")) + let out = () + out = _add(out, shade-preset("logo")) + out = _add(out, shade-theme(options.at("theme"))) + if line-length != none { + out.push(_config.residues-per-line(line-length)) + } + out = _apply-window(out, region, sequence) + out = _add(out, sequence-logo(position: "top", colors: colors)) + if options.at("subfamily") != none { + out.push(_config.subfamily(options.at("subfamily"))) + out.push(_config.subfamily-logo-track(position: "bottom", colorset: colors)) + } + let negative = if options.at("negative") == auto { options.at("subfamily") != none } else { options.at("negative") } + if negative { + out.push(_config.negative-logo-values()) + } + let relevance = if options.at("relevance") == auto and options.at("subfamily") != none { 1.0 } else { options.at("relevance") } + if relevance != none and relevance != auto { + if type(relevance) == dictionary { + out.push(_config.relevance-marker( + char: relevance.at("char", default: "*"), + color: relevance.at("color", default: "Black"), + )) + if relevance.at("threshold", default: none) != none { + out.push(_config.relevance-threshold(relevance.at("threshold"))) + } + } else { + out.push(_config.relevance-threshold(relevance)) + } + } + out = _apply-consensus(out, conservation) + out = _add(out, options.at("commands")) + out +} + +#let overview( + mode: "similar", + colors: none, + line-length: auto, + names: true, + numbers: false, + conservation: false, + ruler: false, + theme: none, + commands: (), +) = _recipe("overview", ( + mode: mode, + colors: colors, + line-length: line-length, + names: names, + numbers: numbers, + conservation: conservation, + ruler: ruler, + theme: theme, + commands: commands, +)) + +#let _build-overview(alignment, options) = { + let line-length = if options.at("line-length") == auto { + _auto-line-length(alignment, purpose: "overview") + } else { + options.at("line-length") + } + let out = () + out = _add(out, shade-preset("overview")) + out = _add(out, shade-theme(options.at("theme"))) + out = _apply-scoring(out, options.at("mode"), options.at("colors"), none) + if line-length != none { + out.push(_config.residues-per-line(line-length)) + } + let names = options.at("names") + if names == false { + out.push(_config.no-names-track()) + } else if names != true { + out.push(_config.names-track(position: _track-position(names, "left"), color: _value(names, "color", none))) + } + let numbers = options.at("numbers") + if numbers == true or type(numbers) == dictionary or type(numbers) == str { + out.push(_config.numbering-track(position: _track-position(numbers, "right"), color: _value(numbers, "color", none))) + } + out = _apply-consensus(out, options.at("conservation")) + out = _apply-ruler(out, options.at("ruler"), 1, 10) + out = _add(out, options.at("commands")) + out +} + +#let _build-recipe(alignment, item) = { + let name = item.at("name") + let options = item.at("options") + if name == "publication" { + _build-publication(alignment, options) + } else if name == "motif-map" { + _build-motif-map(alignment, options) + } else if name == "structure-map" { + _build-structure-map(alignment, options) + } else if name == "logo-analysis" { + _build-logo-analysis(alignment, options) + } else if name == "overview" { + _build-overview(alignment, options) + } else { + () + } +} + +#let resolve-figure(source, format, figure) = { + let out = () + let items = if type(figure) == array { figure } else { (figure,) } + let needs-alignment = false + for item in items { + if _is-recipe(item) { + needs-alignment = true + } + } + let alignment = if needs-alignment { read-alignment(source, format: format) } else { none } + for item in items { + if _is-recipe(item) { + out = _add(out, _build-recipe(alignment, item)) + } else { + out = _add(out, item) + } + } + out +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/selection.typ b/packages/preview/typshade/0.1.3/internal/interface/selection.typ new file mode 100644 index 0000000000..0a8db3943a --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/selection.typ @@ -0,0 +1,64 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#let _selection(op, fields) = { + let out = fields + out.insert("kind", "typshade-selection") + out.insert("op", op) + out +} + +#let select(..items, mode: "or", padding: 0) = _selection(mode, ( + items: items.pos(), + padding: padding, +)) + +#let select-or(..items, padding: 0) = select(..items.pos(), mode: "or", padding: padding) + +#let select-and(..items, padding: 0) = select(..items.pos(), mode: "and", padding: padding) + +#let select-not(selection) = _selection("not", (selection: selection)) + +#let select-pad(selection, before, after: none) = _selection("pad", ( + selection: selection, + before: before, + after: if after == none { before } else { after }, +)) + +#let select-all() = _selection("all", (:)) + +#let select-range(start, ..args) = { + let positional = args.pos() + let stop = if positional.len() > 0 { positional.first() } else { args.named().at("stop", default: none) } + if stop == none and type(start) == str { + _selection("range", (range: start)) + } else { + _selection("range", (start: start, stop: if stop == none { start } else { stop })) + } +} + +#let select-residues(..positions) = _selection("positions", (positions: positions.pos())) + +#let select-motif(pattern) = _selection("motif", (pattern: pattern)) + +#let select-metric( + metric, + above: none, + below: none, + at-least: none, + at-most: none, + min: none, + max: none, + equals: none, + selection: "all", +) = _selection("metric", ( + metric: metric, + above: above, + below: below, + at-least: at-least, + at-most: at-most, + min: min, + max: max, + equals: equals, + selection: selection, +)) diff --git a/packages/preview/typshade/0.1.3/internal/interface/shade.typ b/packages/preview/typshade/0.1.3/internal/interface/shade.typ new file mode 100644 index 0000000000..1415770c2f --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/shade.typ @@ -0,0 +1,163 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config +#import "../engine/commands.typ" as _commands +#import "recipes.typ" as _recipes +#import "presets.typ": shade-preset, shade-theme +#import "../render/alignment.typ" as _render + +#let _typst-figure = figure + +#let _position-value(value, default) = if value == true { + default +} else { + value +} + +#let _value-or-default(value, key, default) = if type(value) == dictionary { + value.at(key, default: default) +} else { + default +} + +#let shade( + source, + format: auto, + figure: (), + preset: none, + theme: none, + mode: none, + option: none, + seq-type: auto, + residues-per-line: none, + fit: none, + names: none, + numbering: none, + consensus: none, + ruler: none, + logo: none, + subfamily-logo: none, + legend: none, + regions: (), + features: (), + commands: (), + caption: none, + short-caption: none, + font: none, + font-size: none, +) = { + let out = () + out = _commands._add-command(out, _recipes.resolve-figure(source, format, figure)) + out = _commands._add-command(out, shade-preset(preset)) + out = _commands._add-command(out, shade-theme(theme)) + if seq-type != auto and seq-type != none { + out.push(_config.sequence-type(seq-type)) + } + if mode != none { + out.push(_config.scoring-mode(mode, option: option)) + } + if residues-per-line != none { + out.push(_config.residues-per-line(residues-per-line)) + } + if fit != none { + if type(fit) == dictionary { + let fit-mode = fit.at("mode", default: fit.at("fit", default: "container")) + out.push(_config.auto-layout( + fit: fit-mode, + min: fit.at("min", default: 1), + max: fit.at("max", default: none), + )) + if fit-mode == "page" or fit.at("page", default: false) != false { + let page = fit.at("page", default: false) + if type(page) == dictionary { + out.push(_config.auto-page(blocks: page.at("blocks", default: auto), repeat-legend: page.at("repeat-legend", default: true))) + } else { + out.push(_config.auto-page()) + } + } + } else if fit != false { + out.push(_config.auto-layout(fit: if fit == true { "container" } else { fit })) + if fit == "page" { + out.push(_config.auto-page()) + } + } + } + if names != none { + if names == false { + out.push(_config.no-names-track()) + } else { + out.push(_config.names-track(position: _position-value(names, "left"))) + } + } + if numbering != none { + if numbering == false { + out.push(_config.no-numbering-track()) + } else { + out.push(_config.numbering-track(position: _position-value(numbering, "right"))) + } + } + if consensus != none { + if consensus == false { + out.push(_config.no-consensus-track()) + } else { + out.push(_config.consensus-track(position: _position-value(consensus, "bottom"))) + } + } + if ruler != none { + if ruler == false { + out.push(_config.no-ruler-track()) + } else if type(ruler) == dictionary { + out.push(_config.ruler-track( + position: ruler.at("position", default: "top"), + sequence: ruler.at("sequence", default: 1), + steps: ruler.at("steps", default: none), + color: ruler.at("color", default: none), + )) + } else { + out.push(_config.ruler-track(position: _position-value(ruler, "top"))) + } + } + if logo != none { + if logo == false { + out.push(_config.no-sequence-logo-track()) + } else if type(logo) == dictionary { + out.push(_config.sequence-logo-track(position: logo.at("position", default: "top"), colorset: logo.at("colorset", default: none))) + } else { + out.push(_config.sequence-logo-track(position: _position-value(logo, "top"))) + } + } + if subfamily-logo != none { + if subfamily-logo == false { + out.push(_config.no-subfamily-logo-track()) + } else if type(subfamily-logo) == dictionary { + if subfamily-logo.keys().contains("sequences") { + out.push(_config.subfamily(subfamily-logo.at("sequences"))) + } + out.push(_config.subfamily-logo-track(position: subfamily-logo.at("position", default: "top"), colorset: subfamily-logo.at("colorset", default: none))) + } else { + out.push(_config.subfamily-logo-track(position: _position-value(subfamily-logo, "top"))) + } + } + if legend != none { + if legend == false { + out.push(_config.no-legend-track()) + } else if type(legend) == dictionary { + out.push(_config.legend-track(color: legend.at("color", default: "Black"))) + } else { + out.push(_config.legend-track()) + } + } + out = _commands._add-command(out, regions) + out = _commands._add-command(out, features) + out = _commands._add-command(out, commands) + if short-caption != none { + out.push(_config.short-caption(short-caption)) + } + let rendered = _render.render-alignment(source, format: format, commands: out, font: font, font-size: font-size) + if caption == none { + rendered + } else { + _typst-figure(rendered, caption: caption) + } +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/shortcuts.typ b/packages/preview/typshade/0.1.3/internal/interface/shortcuts.typ new file mode 100644 index 0000000000..f9f4d097eb --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/shortcuts.typ @@ -0,0 +1,164 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config +#import "annotations.typ": highlight, tint, emphasize, mark, motif, graph +#import "tracks.typ": consensus-track, ruler-track, sequence-logo, legend-track, structure-tracks + +#let _scoring( + mode, + colors: none, + threshold: none, + option: none, + all-match-threshold: none, + weight-table: none, + gap-penalty: none, +) = { + let out = (_config.scoring-mode(mode, option: option),) + if colors != none { + out.push(_config.color-scheme(colors)) + } + if threshold != none { + out.push(_config.threshold(threshold)) + } + if all-match-threshold != none { + out.push(_config.all-match-threshold(value: all-match-threshold)) + } + if weight-table != none { + out.push(_config.weight-table(weight-table)) + } + if gap-penalty != none { + out.push(_config.gap-penalty(gap-penalty)) + } + out +} + +#let identical(colors: none, threshold: none, option: none, ..rest) = _scoring( + "identical", + colors: colors, + threshold: threshold, + option: option, + ..rest, +) + +#let similar(colors: none, threshold: none, option: none, ..rest) = _scoring( + "similar", + colors: colors, + threshold: threshold, + option: option, + ..rest, +) + +#let diverse(colors: none, threshold: none, option: none, ..rest) = _scoring( + "diverse", + colors: colors, + threshold: threshold, + option: option, + ..rest, +) + +#let functional(kind, colors: none, threshold: none, ..rest) = _scoring( + "functional", + colors: colors, + threshold: threshold, + option: kind, + ..rest, +) + +#let single-sequence(colors: none, threshold: none, sequence: none, ..rest) = _scoring( + "singleseq", + colors: colors, + threshold: threshold, + option: sequence, + ..rest, +) + +#let tcoffee(source) = _config.scoring-mode("T-Coffee", option: source) + +#let lines(count) = _config.residues-per-line(count) + +#let window(sequence, selection, start: none) = _config.sequence-window(sequence, selection, start: start) + +#let names(position: "left", color: none) = _config.names-track(position: position, color: color) + +#let no-names() = _config.no-names-track() + +#let numbers(position: "right", color: none) = _config.numbering-track(position: position, color: color) + +#let no-numbers() = _config.no-numbering-track() + +#let consensus(position, scale: none, name: none) = consensus-track(position: position, scale: scale, name: name) + +#let no-consensus() = _config.no-consensus-track() + +#let ruler(position, sequence: 1, every: none, steps: none, color: none, name: none, name-color: none, space: none) = { + ruler-track( + position: position, + sequence: sequence, + steps: if steps == none { every } else { steps }, + color: color, + name: name, + name-color: name-color, + space: space, + ) +} + +#let no-ruler(position: none) = _config.no-ruler-track(position: position) + +#let logo(position, colors: none, name: none, scale: "leftright", relevance-marker: none, stretch: none) = { + sequence-logo( + position: position, + colors: colors, + name: name, + scale: scale, + relevance-marker: relevance-marker, + stretch: stretch, + ) +} + +#let no-logo() = _config.no-sequence-logo-track() + +#let legend(color: "Black") = legend-track(color: color) + +#let no-legend() = _config.no-legend-track() + +#let structures(sequence, hmmtop: none, topology: none, secondary: none, hmmtop-sequence: none) = { + structure-tracks( + sequence, + hmmtop: hmmtop, + topology: topology, + secondary: secondary, + hmmtop-sequence: hmmtop-sequence, + ) +} + +#let gap-style(foreground: none, background: none, rule: none) = { + let out = () + if foreground != none or background != none { + out.push(_config.gap-colors( + if foreground == none { "Black" } else { foreground }, + if background == none { "White" } else { background }, + )) + } + if rule != none { + out.push(_config.gap-rule(rule)) + } + out +} + +#let typography(target: "all", family: none, weight: none, posture: none, size: none) = { + let out = () + if family != none { + out.push(_config.text-family(target, family)) + } + if weight != none { + out.push(_config.text-weight(target, weight)) + } + if posture != none { + out.push(_config.text-posture(target, posture)) + } + if size != none { + out.push(_config.text-size(target, size)) + } + out +} diff --git a/packages/preview/typshade/0.1.3/internal/interface/tracks.typ b/packages/preview/typshade/0.1.3/internal/interface/tracks.typ new file mode 100644 index 0000000000..39923dd96a --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/interface/tracks.typ @@ -0,0 +1,77 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ" as _config + +#let consensus-track(position: "bottom", scale: none, name: none) = _config.consensus-track(position: position, scale: scale, name: name) +#let no-consensus() = _config.no-consensus-track() + +#let ruler-track(position: "top", sequence: 1, steps: none, color: none, name: none, name-color: none, space: none) = { + let out = (_config.ruler-track(position: position, sequence: sequence, steps: steps, color: color),) + if name != none { + out.push(_config.ruler-name(name, position: position)) + } + if color != none { + out.push(_config.ruler-color(color, position: position)) + } + if name-color != none { + out.push(_config.ruler-name-color(name-color, position: position)) + } + if space != none { + out.push(_config.ruler-space(space, position: position)) + } + out +} + +#let ruler-marker(number, text, position: "top", color: none) = _config.ruler-marker(number, text, position: position, color: color) +#let no-ruler(position: none) = _config.no-ruler-track(position: position) + +#let sequence-logo(position: "top", colors: none, name: none, scale: "leftright", relevance-marker: none, stretch: none) = { + let out = (_config.sequence-logo-track(position: position, colorset: colors),) + if name != none { + out.push(_config.sequence-logo-name(name)) + } + if scale == false { + out.push(_config.no-logo-scale()) + } else if scale != none { + out.push(_config.logo-scale(position: scale)) + } + if relevance-marker != none { + out.push(_config.relevance-marker(char: relevance-marker.at("char", default: "*"), color: relevance-marker.at("color", default: "Black"))) + if relevance-marker.at("threshold", default: none) != none { + out.push(_config.relevance-threshold(relevance-marker.at("threshold"))) + } + } + if stretch != none { + out.push(_config.logo-stretch(stretch)) + } + out +} + +#let no-sequence-logo() = _config.no-sequence-logo-track() + +#let subfamily-logo(sequences, position: "bottom", colors: none, name: none, negative-name: none) = { + let out = (_config.subfamily(sequences), _config.subfamily-logo-track(position: position, colorset: colors)) + if name != none or negative-name != none { + out.push(_config.subfamily-logo-name(if name == none { "subfamily" } else { name }, negative-name: negative-name)) + } + out +} + +#let no-subfamily-logo() = _config.no-subfamily-logo-track() + +#let legend-track(color: "Black") = _config.legend-track(color: color) + +#let structure-tracks(sequence, hmmtop: none, topology: none, secondary: none, hmmtop-sequence: none) = { + let out = () + if hmmtop != none { + out.push(_config.hmmtop-track(sequence, hmmtop, source-sequence: hmmtop-sequence)) + } + if topology != none { + out.push(_config.phd-topology-track(sequence, topology)) + } + if secondary != none { + out.push(_config.phd-secondary-track(sequence, secondary)) + } + out +} diff --git a/packages/preview/typshade/0.1.3/internal/model/logo.typ b/packages/preview/typshade/0.1.3/internal/model/logo.typ new file mode 100644 index 0000000000..9e16680b83 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/model/logo.typ @@ -0,0 +1,181 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "palette.typ": _default-logo-colors, _functional-presets, _logo-color-presets +#import "parser.typ": _upper + +#let _log2(value) = calc.log(value) / calc.log(2) +#let _logo-max-bits(seq-type) = if seq-type == "N" { 2.0 } else { _log2(20.0) } + +#let _sequence-reference-message(alignment) = { + let names = alignment.at("sequences").map(seq => seq.at("name")).join(", ") + "expected a 1-based sequence index from 1 to " + str(alignment.at("sequences").len()) + " or one of: " + names +} + +#let _resolve-sequence(alignment, sequence) = { + if type(sequence) == int { + assert( + sequence >= 1 and sequence <= alignment.at("sequences").len(), + message: "typshade: sequence index `" + str(sequence) + "` is out of range; " + _sequence-reference-message(alignment), + ) + return sequence - 1 + } + let ref = str(sequence) + for (idx, seq) in alignment.at("sequences").enumerate() { + if seq.at("name") == ref { + return idx + } + } + if ref.matches(regex("^\\d+$")).len() > 0 { + let index = int(ref) + assert( + index >= 1 and index <= alignment.at("sequences").len(), + message: "typshade: sequence index `" + ref + "` is out of range; " + _sequence-reference-message(alignment), + ) + return index - 1 + } + assert( + false, + message: "typshade: unknown sequence `" + ref + "`; " + _sequence-reference-message(alignment), + ) +} + +#let _functional-style(residue, mode) = { + if not _functional-presets.keys().contains(mode) { + return none + } + for group in _functional-presets.at(mode) { + if group.at("residues").contains(residue) { + return group + } + } + none +} + +#let _logo-preset(seq-type, colorset) = { + if colorset == none { + return if seq-type == "N" { "nucleotide" } else { "rasmol" } + } + let key = str(colorset) + if key == "standard" { + return if seq-type == "N" { "nucleotide" } else { "rasmol" } + } + key +} + +#let _logo-frequencies(alignment, col, subset: none, subfamily: false) = { + let counts = (:) + let rest = (:) + let total = 0 + let total-rest = 0 + let use-all = subset == none or subset.len() == 0 + for (idx, seq) in alignment.at("sequences").enumerate() { + let char = _upper(seq.at("aligned").slice(col, col + 1)) + if char == "." or char == "-" or char == "" { + continue + } + let in-subset = use-all or subset.contains(seq.at("name")) or subset.contains(idx + 1) + if not subfamily { + if in-subset { + counts.insert(char, counts.at(char, default: 0) + 1) + total += 1 + } + } else if in-subset { + counts.insert(char, counts.at(char, default: 0) + 1) + total += 1 + } else { + rest.insert(char, rest.at(char, default: 0) + 1) + total-rest += 1 + } + } + if not subfamily { + return (counts: counts, total: total) + } + let diffs = (:) + for key in counts.keys() { + let left = if total == 0 { 0.0 } else { counts.at(key) / total } + let right = if total-rest == 0 { 0.0 } else { rest.at(key, default: 0) / total-rest } + diffs.insert(key, left - right) + } + for key in rest.keys() { + if not diffs.keys().contains(key) { + let right = if total-rest == 0 { 0.0 } else { rest.at(key) / total-rest } + diffs.insert(key, -right) + } + } + (counts: diffs, total: 1) +} + +#let _logo-color(seq-type, colorset, residue) = { + let key = _upper(residue) + if type(colorset) == dictionary and colorset.keys().contains(key) { + return colorset.at(key) + } + let preset = _logo-preset(seq-type, colorset) + if _logo-color-presets.keys().contains(preset) { + for entry in _logo-color-presets.at(preset) { + if entry.at("residues").contains(key) { + return entry.at("color") + } + } + } + if colorset != none and _functional-presets.keys().contains(colorset) { + let group = _functional-style(key, colorset) + if group != none { + return group.at("bg") + } + } + _default-logo-colors.at(seq-type).at(key, default: "Black") +} + +#let _logo-colorset(config, residue, subfamily: false) = { + let custom = config.at("logo-colors").at("map") + let key = _upper(residue) + if custom.keys().contains(key) or config.at("logo-colors").at("default") != none { + return custom + (default: config.at("logo-colors").at("default")) + } + if subfamily { + return config.at("subfamily-logo").at("colorset") + } + config.at("sequence-logo").at("colorset") +} + +#let _logo-residue-color(config, seq-type, residue, subfamily: false) = { + _logo-color(seq-type, _logo-colorset(config, residue, subfamily: subfamily), residue) +} + +#let _logo-column-items(alignment, config, col, subfamily: false) = { + let subset = if subfamily { config.at("subfamily") } else { none } + let data = _logo-frequencies(alignment, col, subset: subset, subfamily: subfamily) + let counts = data.at("counts") + if counts.keys().len() == 0 { + return () + } + let seq-type = alignment.at("seq-type") + let heights = () + if not subfamily { + let total = data.at("total") + let entropy = 0.0 + for key in counts.keys() { + let p = counts.at(key) / total + if p > 0 { + entropy += -p * _log2(p) + } + } + let correction = if config.at("frequency-correction") and total > 0 { + (counts.keys().len() - 1) / (2.0 * calc.log(2) * total) + } else { + 0.0 + } + let info = calc.max(0.0, _logo-max-bits(seq-type) - entropy - correction) + for key in counts.keys() { + let p = counts.at(key) / total + heights.push((residue: key, value: p * info)) + } + } else { + for key in counts.keys() { + heights.push((residue: key, value: counts.at(key) * _logo-max-bits(seq-type))) + } + } + heights.sorted(key: it => calc.abs(it.at("value"))).filter(it => calc.abs(it.at("value")) > 0) +} diff --git a/packages/preview/typshade/0.1.3/internal/model/palette.typ b/packages/preview/typshade/0.1.3/internal/model/palette.typ new file mode 100644 index 0000000000..88d5b3239e --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/model/palette.typ @@ -0,0 +1,460 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#let _color-db = ( + Black: (0, 0, 0), + White: (255, 255, 255), + Gray: (128, 128, 128), + Gray0: (255, 255, 255), + Gray5: (242, 242, 242), + Gray10: (230, 230, 230), + Gray15: (217, 217, 217), + Gray20: (204, 204, 204), + Gray25: (191, 191, 191), + Gray30: (179, 179, 179), + LightGray: (171, 171, 171), + Gray35: (166, 166, 166), + Gray40: (153, 153, 153), + Gray45: (140, 140, 140), + Gray50: (128, 128, 128), + Gray55: (115, 115, 115), + Gray60: (102, 102, 102), + Gray65: (89, 89, 89), + DarkGray: (87, 87, 87), + Gray70: (77, 77, 77), + Gray75: (64, 64, 64), + Gray80: (51, 51, 51), + Gray85: (38, 38, 38), + Gray90: (26, 26, 26), + Gray95: (13, 13, 13), + Red: (255, 0, 0), + Blue: (0, 0, 255), + Green: (0, 255, 0), + Magenta: (255, 0, 255), + Cyan: (0, 255, 255), + Yellow: (255, 255, 0), + Orange: (255, 99, 33), + YellowOrange: (255, 148, 0), + YellowGreen: (217, 255, 79), + Goldenrod: (255, 230, 41), + GreenYellow: (217, 255, 79), + RoyalBlue: (0, 128, 255), + RoyalPurple: (64, 25, 255), + PineGreen: (15, 191, 105), + OliveGreen: (55, 153, 8), + BrickRed: (184, 28, 15), + Mahagony: (166, 25, 22), + Brown: (102, 19, 0), + MidnightBlue: (3, 126, 255), + CornflowerBlue: (89, 222, 255), + Apricot: (255, 173, 122), + SkyBlue: (97, 255, 224), + RedViolet: (157, 17, 168), + CarnationPink: (255, 166, 255), + LightMagenta: (255, 128, 255), + LightBlue: (128, 128, 255), + LightGreen: (128, 255, 128), + LightRed: (255, 128, 128), + LightBrown: (179, 107, 89), + TC0: (153, 153, 255), + TC1: (102, 255, 77), + TC2: (153, 255, 77), + TC3: (204, 255, 0), + TC4: (255, 255, 0), + TC5: (255, 204, 0), + TC6: (255, 153, 0), + TC7: (255, 102, 0), + TC8: (255, 51, 0), + TC9: (255, 32, 0), + BlackWhite0: (0, 0, 0), + BlackWhite5: (13, 13, 13), + BlackWhite10: (26, 26, 26), + BlackWhite15: (38, 38, 38), + BlackWhite20: (51, 51, 51), + BlackWhite25: (64, 64, 64), + BlackWhite30: (77, 77, 77), + BlackWhite35: (89, 89, 89), + BlackWhite40: (102, 102, 102), + BlackWhite45: (115, 115, 115), + BlackWhite50: (128, 128, 128), + BlackWhite55: (140, 140, 140), + BlackWhite60: (153, 153, 153), + BlackWhite65: (166, 166, 166), + BlackWhite70: (179, 179, 179), + BlackWhite75: (191, 191, 191), + BlackWhite80: (204, 204, 204), + BlackWhite85: (217, 217, 217), + BlackWhite90: (230, 230, 230), + BlackWhite95: (242, 242, 242), + BlackWhite100: (255, 255, 255), + WhiteBlack0: (255, 255, 255), + WhiteBlack5: (242, 242, 242), + WhiteBlack10: (230, 230, 230), + WhiteBlack15: (217, 217, 217), + WhiteBlack20: (204, 204, 204), + WhiteBlack25: (191, 191, 191), + WhiteBlack30: (179, 179, 179), + WhiteBlack35: (166, 166, 166), + WhiteBlack40: (153, 153, 153), + WhiteBlack45: (140, 140, 140), + WhiteBlack50: (128, 128, 128), + WhiteBlack55: (115, 115, 115), + WhiteBlack60: (102, 102, 102), + WhiteBlack65: (89, 89, 89), + WhiteBlack70: (77, 77, 77), + WhiteBlack75: (64, 64, 64), + WhiteBlack80: (51, 51, 51), + WhiteBlack85: (38, 38, 38), + WhiteBlack90: (26, 26, 26), + WhiteBlack95: (13, 13, 13), + WhiteBlack100: (0, 0, 0), + BrewerA: (51, 255, 0), + BrewerC: (166, 237, 255), + BrewerG: (25, 179, 255), + BrewerT: (179, 255, 140), +) + +#let _scale-db = ( + BlueRed: ( + (0, (26, 26, 128)), + (25, (110, 110, 173)), + (50, (219, 209, 209)), + (75, (232, 99, 94)), + (100, (222, 41, 10)), + ), + RedBlue: ( + (0, (222, 38, 10)), + (25, (232, 99, 94)), + (50, (219, 209, 209)), + (75, (110, 110, 173)), + (100, (38, 43, 140)), + ), + GreenRed: ( + (0, (0, 255, 0)), + (50, (115, 140, 0)), + (100, (242, 13, 0)), + ), + RedGreen: ( + (0, (255, 0, 0)), + (50, (140, 115, 0)), + (100, (13, 242, 0)), + ), + ColdHot: ( + (0, (0, 0, 255)), + (25, (0, 230, 255)), + (50, (0, 255, 10)), + (75, (250, 255, 0)), + (100, (232, 0, 0)), + ), + HotCold: ( + (0, (255, 0, 0)), + (25, (255, 153, 0)), + (50, (41, 255, 0)), + (75, (0, 255, 222)), + (100, (0, 20, 255)), + ), + TCoffee: ( + (0, (153, 153, 255)), + (10, (102, 255, 77)), + (20, (153, 255, 77)), + (30, (204, 255, 0)), + (40, (255, 255, 0)), + (50, (255, 204, 0)), + (60, (255, 153, 0)), + (70, (255, 102, 0)), + (80, (255, 51, 0)), + (90, (255, 32, 0)), + (100, (255, 32, 0)), + ), +) + +#let _pep-groups = ( + "FYW", + "ILVM", + "RK", + "DE", + "GA", + "ST", + "NQ", +) + +#let _pep-sims = ( + F: "YW", + Y: "WF", + W: "YF", + I: "LVM", + L: "VMI", + V: "MIL", + R: "KH", + K: "HR", + H: "RK", + A: "GS", + G: "A", + S: "TA", + T: "S", + D: "EN", + E: "DQ", + N: "QD", + Q: "NE", +) + +#let _dna-groups = ( + "GAR", + "CTY", +) + +#let _dna-sims = ( + A: "GR", + G: "AR", + R: "AG", + C: "TY", + T: "CY", + Y: "CT", +) + +#let _functional-presets = ( + charge: ( + (name: "acidic (-)", residues: "DE", fg: "White", bg: "Red"), + (name: "basic (+)", residues: "KRH", fg: "White", bg: "Blue"), + ), + hydropathy: ( + (name: "acidic (-)", residues: "DE", fg: "White", bg: "Red"), + (name: "basic (+)", residues: "KRH", fg: "White", bg: "Blue"), + (name: "polar uncharged", residues: "YSTGNQC", fg: "Black", bg: "Yellow"), + (name: "hydrophobic nonpolar", residues: "AFPMWVIL", fg: "White", bg: "Green"), + ), + structure: ( + (name: "external", residues: "DEHKNQR", fg: "Black", bg: "Orange"), + (name: "ambivalent", residues: "ACGPSTWY", fg: "Black", bg: "Yellow"), + (name: "internal", residues: "FILMV", fg: "White", bg: "Green"), + ), + chemical: ( + (name: "acidic (-)", residues: "DE", fg: "White", bg: "Red"), + (name: "aliphatic", residues: "AGVIL", fg: "White", bg: "Black"), + (name: "aliphatic (small)", residues: "AG", fg: "White", bg: "Gray"), + (name: "amide", residues: "NQ", fg: "White", bg: "Green"), + (name: "aromatic", residues: "FYW", fg: "White", bg: "Brown"), + (name: "basic (+)", residues: "KRH", fg: "White", bg: "Blue"), + (name: "hydroxyl", residues: "ST", fg: "Black", bg: "Magenta"), + (name: "imino", residues: "P", fg: "Black", bg: "Orange"), + (name: "sulfur", residues: "CM", fg: "Black", bg: "Yellow"), + ), + rasmol: ( + (name: "Asp, Glu", residues: "DE", fg: "Red", bg: "White"), + (name: "Arg, Lys, His", residues: "KRH", fg: "Blue", bg: "White"), + (name: "Phe, Tyr, Trp", residues: "FYW", fg: "MidnightBlue", bg: "White"), + (name: "Ala, Gly", residues: "AG", fg: "Gray", bg: "White"), + (name: "Cys, Met", residues: "CM", fg: "Yellow", bg: "White"), + (name: "Ser, Thr", residues: "ST", fg: "Orange", bg: "White"), + (name: "Asn, Gln", residues: "NQ", fg: "Cyan", bg: "White"), + (name: "Leu, Val, Ile", residues: "LVI", fg: "Green", bg: "White"), + (name: "Pro", residues: "P", fg: "Apricot", bg: "White"), + ), + DNA: ( + (name: "C", residues: "Cc", fg: "Black", bg: "BrewerC"), + (name: "G", residues: "Gg", fg: "White", bg: "BrewerG"), + (name: "A", residues: "Aa", fg: "Black", bg: "BrewerA"), + (name: "T,U", residues: "TtUu", fg: "Black", bg: "BrewerT"), + ), +) + +#let _default-logo-colors = ( + P: ( + D: "Red", E: "Red", + C: "Yellow", M: "Yellow", + K: "Blue", R: "Blue", + S: "Orange", T: "Orange", + F: "MidnightBlue", Y: "MidnightBlue", + N: "Cyan", Q: "Cyan", + G: "LightGray", + L: "Green", V: "Green", I: "Green", + A: "DarkGray", + W: "LightMagenta", + H: "CornflowerBlue", + P: "Apricot", + B: "LightMagenta", Z: "LightMagenta", + ), + N: ( + G: "Black", + A: "Green", + T: "Red", + U: "Red", + C: "Blue", + ), +) + +#let _logo-color-presets = ( + nucleotide: ( + (residues: "G", color: "Black"), + (residues: "A", color: "Green"), + (residues: "TU", color: "Red"), + (residues: "C", color: "Blue"), + ), + rasmol: ( + (residues: "DE", color: "Red"), + (residues: "CM", color: "Yellow"), + (residues: "KR", color: "Blue"), + (residues: "ST", color: "Orange"), + (residues: "FY", color: "MidnightBlue"), + (residues: "NQ", color: "Cyan"), + (residues: "G", color: "LightGray"), + (residues: "LVI", color: "Green"), + (residues: "A", color: "DarkGray"), + (residues: "W", color: "CarnationPink"), + (residues: "H", color: "CornflowerBlue"), + (residues: "P", color: "Apricot"), + (residues: "BZ", color: "LightMagenta"), + ), + chemical: ( + (residues: "DE", color: "Red"), + (residues: "VIL", color: "Black"), + (residues: "AG", color: "Gray"), + (residues: "NQ", color: "Green"), + (residues: "FYW", color: "Brown"), + (residues: "KRH", color: "Blue"), + (residues: "ST", color: "Magenta"), + (residues: "P", color: "Orange"), + (residues: "CM", color: "Yellow"), + ), + hydropathy: ( + (residues: "DE", color: "Red"), + (residues: "KRH", color: "Blue"), + (residues: "YSTGNQC", color: "Yellow"), + (residues: "AFPMWVIL", color: "Green"), + ), + structure: ( + (residues: "DEHKNQR", color: "Orange"), + (residues: "ACGPSTWY", color: "Yellow"), + (residues: "FILMV", color: "Green"), + ), + "standard area": ( + (residues: "G", color: "BrickRed"), + (residues: "AS", color: "Orange"), + (residues: "CP", color: "Yellow"), + (residues: "TDVN", color: "YellowGreen"), + (residues: "IE", color: "PineGreen"), + (residues: "LQHM", color: "SkyBlue"), + (residues: "FK", color: "RoyalPurple"), + (residues: "Y", color: "RedViolet"), + (residues: "RW", color: "Black"), + ), + "accessible area": ( + (residues: "C", color: "BrickRed"), + (residues: "IVG", color: "Orange"), + (residues: "FLMA", color: "Yellow"), + (residues: "WSTH", color: "YellowGreen"), + (residues: "P", color: "PineGreen"), + (residues: "YDN", color: "SkyBlue"), + (residues: "EQ", color: "RoyalPurple"), + (residues: "R", color: "RedViolet"), + (residues: "K", color: "Black"), + ), + hardness: ( + (residues: "ADEGILPQSTV", color: "BlueRed5"), + (residues: "KN", color: "BlueRed20"), + (residues: "R", color: "BlueRed40"), + (residues: "CFH", color: "BlueRed60"), + (residues: "MY", color: "BlueRed80"), + (residues: "W", color: "BlueRed100"), + ), + DNA: ( + (residues: "A", color: "BrewerA"), + (residues: "C", color: "BrewerC"), + (residues: "G", color: "BrewerG"), + (residues: "TU", color: "BrewerT"), + ), +) + +#let _rgb(tuple) = rgb(int(calc.round(tuple.at(0))), int(calc.round(tuple.at(1))), int(calc.round(tuple.at(2)))) + +#let _mix-rgb(a, b, ratio) = ( + int(calc.round(a.at(0) + (b.at(0) - a.at(0)) * ratio)), + int(calc.round(a.at(1) + (b.at(1) - a.at(1)) * ratio)), + int(calc.round(a.at(2) + (b.at(2) - a.at(2)) * ratio)), +) + +#let _color-name(value) = if type(value) == str { value } else { str(value) } + +#let _rgb-components(value, default: "Black") = { + if value == none { + return _color-db.at(default) + } + let key = _color-name(value) + if _color-db.keys().contains(key) { + return _color-db.at(key) + } + let scale-hit = key.matches(regex("^([A-Za-z]+)(\\d+)$")) + if scale-hit.len() > 0 and _scale-db.keys().contains(scale-hit.first().captures.at(0)) { + let level = int(scale-hit.first().captures.at(1)) + let points = _scale-db.at(scale-hit.first().captures.at(0)) + if level <= points.first().at(0) { + return points.first().at(1) + } + if level >= points.last().at(0) { + return points.last().at(1) + } + for idx in range(0, points.len() - 1) { + let left = points.at(idx) + let right = points.at(idx + 1) + if level >= left.at(0) and level <= right.at(0) { + let span = right.at(0) - left.at(0) + let ratio = if span == 0 { 0.0 } else { (level - left.at(0)) / span } + return _mix-rgb(left.at(1), right.at(1), ratio) + } + } + } + _color-db.at(default) +} + +#let resolve-color(value, default: "Black") = { + if value == none { + return _rgb(_color-db.at(default)) + } + if type(value) == color { + return value + } + let key = _color-name(value) + if key.starts-with("#") { + return rgb(key) + } + if key.starts-with("rgb(") { + return rgb(key) + } + let scale-hit = key.matches(regex("^([A-Za-z]+)(\\d+)$")) + if scale-hit.len() > 0 and _scale-db.keys().contains(scale-hit.first().captures.at(0)) { + return scale-color(scale-hit.first().captures.at(0), int(scale-hit.first().captures.at(1))) + } + if _color-db.keys().contains(key) { + return _rgb(_color-db.at(key)) + } + black +} + +#let scale-color(name, value) = { + let clamped = calc.max(0, calc.min(100, value)) + let key = _color-name(name) + if key == "BlackWhite" or key == "WhiteBlack" { + let scale-key = key + str(calc.round(clamped / 5) * 5) + return resolve-color(scale-key, default: "Black") + } + if not _scale-db.keys().contains(key) { + return resolve-color("Gray50") + } + let points = _scale-db.at(key) + if clamped <= points.first().at(0) { + return _rgb(points.first().at(1)) + } + if clamped >= points.last().at(0) { + return _rgb(points.last().at(1)) + } + for idx in range(0, points.len() - 1) { + let left = points.at(idx) + let right = points.at(idx + 1) + if clamped >= left.at(0) and clamped <= right.at(0) { + let span = right.at(0) - left.at(0) + let ratio = if span == 0 { 0.0 } else { (clamped - left.at(0)) / span } + return _rgb(_mix-rgb(left.at(1), right.at(1), ratio)) + } + } + _rgb(points.last().at(1)) +} diff --git a/packages/preview/typshade/0.1.3/internal/model/parser.typ b/packages/preview/typshade/0.1.3/internal/model/parser.typ new file mode 100644 index 0000000000..6a55283130 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/model/parser.typ @@ -0,0 +1,519 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#let _translate-case(text, from, to) = { + let out = "" + for ch in text.clusters() { + let idx = from.position(ch) + out += if idx == none { ch } else { to.slice(idx, idx + 1) } + } + out +} + +#let _upper(text) = { + let lower = "abcdefghijklmnopqrstuvwxyz" + let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + _translate-case(str(text), lower, upper) +} + +#let _lower(text) = { + let lower = "abcdefghijklmnopqrstuvwxyz" + let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + _translate-case(str(text), upper, lower) +} + +#let _split-lines(text) = text.replace("\r\n", "\n").replace("\r", "\n").split("\n") + +#let _v15-or-later() = sys.version >= version(0, 15, 0) + +#let _source-is-path(source) = { + if _v15-or-later() { + type(source) == path + } else { + false + } +} + +#let _looks-like-source-text(source) = { + let text = str(source) + let trimmed = text.trim() + trimmed == "" or text.contains("\n") or trimmed.starts-with(">") or trimmed.starts-with("CLUSTAL") or trimmed.starts-with("MUSCLE") or trimmed.starts-with("ATOM") or trimmed.starts-with("HETATM") or text.contains(" MSF: ") or text.contains("!!AA_MULTIPLE_ALIGNMENT") or text.contains("!!NA_MULTIPLE_ALIGNMENT") or trimmed.matches(regex("^\\S+\\s+[A-Za-z\\-\\.]+$")).len() > 0 +} + +#let _source-text(source) = { + if type(source) == bytes { + str(source) + } else if _source-is-path(source) { + str(read(source, encoding: none)) + } else { + assert( + type(source) == str and _looks-like-source-text(source), + message: "typshade: source must be alignment text, bytes, or path(...); use read(..., encoding: none) or path(...) for files", + ) + source + } +} + +#let _strip-empty(lines) = { + let out = () + for line in lines { + if line != "" { + out.push(line) + } + } + out +} + +#let _chars(text) = { + let out = () + for ch in str(text).clusters() { + out.push(ch) + } + out +} + +#let _repeat(value, times) = { + let out = () + for _ in range(0, times) { + out.push(value) + } + out +} + +#let _array-fill(length, value) = { + let out = () + for _ in range(0, length) { + out.push(if type(value) == dictionary { value + (:) } else { value }) + } + out +} + +#let _clone-array(items) = { + let out = () + for item in items { + out.push(item) + } + out +} + +#let _guess-seq-type(sequences) = { + let dna = "ACGTUNRYKMWSBDHV.-" + let protein = "ABCDEFGHIKLMNPQRSTVWXYZOUJ.-" + let dna-only = 0 + let protein-only = 0 + for seq in sequences { + for ch in _chars(_upper(seq.at("aligned"))) { + if protein.contains(ch) { + protein-only += 1 + } + if dna.contains(ch) { + dna-only += 1 + } + } + } + if sequences.len() == 0 or (dna-only > 0 and protein-only == dna-only) { + "N" + } else { + "P" + } +} + +#let _build-sequence(name, aligned, metadata) = { + let positions = () + let raw = "" + let pos = 0 + for ch in _chars(aligned) { + if ch == "." or ch == "-" { + positions.push(none) + } else { + pos += 1 + raw += ch + positions.push(pos) + } + } + ( + name: name, + aligned: aligned, + raw: raw, + positions: positions, + length: pos, + metadata: metadata, + ) +} + +#let _validated-alignment(format, sequences, seq-type, name: "alignment") = { + assert(sequences.len() > 0, message: "typshade: " + format + " alignment is empty") + let columns = sequences.first().at("aligned").len() + for seq in sequences { + let length = seq.at("aligned").len() + assert( + length == columns, + message: "typshade: " + format + " alignment has inconsistent column length for sequence `" + seq.at("name") + "`: expected " + str(columns) + ", got " + str(length), + ) + } + ( + format: format, + name: name, + seq-type: seq-type, + columns: columns, + sequences: sequences, + ) +} + +#let parse-msf(text) = { + let lines = _split-lines(text) + let body = false + let blocks = (:) + let ignored = () + let declared-type = none + for line in lines { + let trimmed = line.trim() + if trimmed == "//" { + body = true + continue + } + if not body { + let ignored-hit = trimmed.matches(regex("^!Name:\\s+(\\S+)")) + if ignored-hit.len() > 0 { + ignored.push(ignored-hit.first().captures.at(0)) + } + let type-hit = trimmed.matches(regex("Type:\\s*([PN])")) + if type-hit.len() > 0 { + declared-type = type-hit.first().captures.at(0) + } + continue + } + if trimmed == "" { + continue + } + let hit = trimmed.matches(regex("^(\\S+)\\s+(.+)$")) + if hit.len() == 0 { + continue + } + let name = hit.first().captures.at(0) + if name.matches(regex("^\\d+$")).len() > 0 { + continue + } + let fragment = hit.first().captures.at(1).replace(" ", "") + if not blocks.keys().contains(name) { + blocks.insert(name, "") + } + blocks.insert(name, blocks.at(name) + fragment) + } + let sequences = () + for name in blocks.keys() { + if not ignored.contains(name) { + sequences.push(_build-sequence(name, blocks.at(name), (:))) + } + } + _validated-alignment( + "MSF", + sequences, + if declared-type == none { _guess-seq-type(sequences) } else { declared-type }, + ) +} + +#let parse-aln(text) = { + let lines = _split-lines(text) + let pieces = (:) + for line in lines { + if line.trim() == "" or line.starts-with("CLUSTAL") or line.starts-with("MUSCLE") or line.starts-with(" ") { + continue + } + let matches = line.matches(regex("^(\\S+)\\s+([A-Za-z\\-\\.]+)")) + if matches.len() == 0 { + continue + } + let hit = matches.first() + let name = hit.captures.at(0) + let fragment = hit.captures.at(1) + if not pieces.keys().contains(name) { + pieces.insert(name, "") + } + pieces.insert(name, pieces.at(name) + fragment) + } + let sequences = () + for name in pieces.keys() { + sequences.push(_build-sequence(name, pieces.at(name), (:))) + } + _validated-alignment("ALN", sequences, _guess-seq-type(sequences)) +} + +#let parse-fasta(text) = { + let lines = _split-lines(text) + let sequences = () + let current-name = none + let current-seq = "" + for line in lines { + if line.starts-with(">") { + if current-name != none { + sequences.push(_build-sequence(current-name, current-seq, (:))) + } + let label = line.slice(1).trim() + current-name = if label == "" { "seq" + str(sequences.len() + 1) } else { label.split(" ").first() } + current-seq = "" + } else if line.trim() != "" { + current-seq += line.trim() + } + } + if current-name != none { + sequences.push(_build-sequence(current-name, current-seq, (:))) + } + _validated-alignment("FASTA", sequences, _guess-seq-type(sequences)) +} + +#let _detected-format(format, text) = { + if format == auto { + if text.trim().starts-with(">") { + return "FASTA" + } + if text.contains(" MSF: ") { + return "MSF" + } + return "ALN" + } + let key = _upper(str(format)) + if key == "AUTO" { + _detected-format(auto, text) + } else if key == "FASTA" or key == "FA" or key == "FAS" { + "FASTA" + } else if key == "MSF" { + "MSF" + } else if key == "ALN" or key == "CLUSTAL" or key == "MUSCLE" { + "ALN" + } else { + assert( + false, + message: "typshade: unknown alignment format `" + str(format) + "`; expected auto, fasta, msf, or aln", + ) + } +} + +#let read-alignment(source, format: auto) = { + let text = _source-text(source) + let detected = _detected-format(format, text) + if detected == "MSF" { + parse-msf(text) + } else if detected == "FASTA" { + parse-fasta(text) + } else { + parse-aln(text) + } +} + +#let read-tcoffee(source) = { + let data = (:) + let lines = _split-lines(_source-text(source)) + for line in lines { + let trimmed = line.trim() + let matches = trimmed.matches(regex("^(\\S+)\\s+\\d+\\s+([A-Za-z0-9\\-]+)\\s+\\d+$")) + if matches.len() == 0 { + continue + } + let name = matches.first().captures.at(0) + let fragment = matches.first().captures.at(1) + let scores = if data.keys().contains(name) { data.at(name) } else { () } + for ch in _chars(fragment) { + if ch == "-" { + scores.push(none) + } else if ch.matches(regex("\\d")).len() > 0 { + scores.push(int(ch) * 10) + } else { + scores.push(0) + } + } + data.insert(name, scores) + } + data +} + +#let _run-lengths(chars, allowed) = { + let regions = () + let active = false + let start = 0 + for idx in range(0, chars.len()) { + let hit = allowed.contains(chars.at(idx)) + if hit and not active { + active = true + start = idx + 1 + } + if active and (not hit or idx == chars.len() - 1) { + let stop = if hit and idx == chars.len() - 1 { idx + 1 } else { idx } + regions.push((start, stop)) + active = false + } + } + regions +} + +#let read-hmmtop(source, sequence: none) = { + let text = _source-text(source) + if text.contains("Transmembrane helices:") { + let name = text.matches(regex("Protein:\\s*(\\S+)")).first().captures.at(0) + let orientation = text.matches(regex("N-terminus:\\s*(\\S+)")).first().captures.at(0) + let spans-line = text.matches(regex("Transmembrane helices:\\s*([0-9\\- ]+)")).first().captures.at(0) + let spans = () + for token in spans-line.split(" ") { + if token.trim() == "" { + continue + } + let hit = token.matches(regex("^(\\d+)-(\\d+)$")).first() + spans.push((int(hit.captures.at(0)), int(hit.captures.at(1)))) + } + return (name: name, orientation: orientation, spans: spans) + } + let lines = _split-lines(text) + for line in lines { + let trimmed = line.trim() + if not trimmed.starts-with(">HP:") { + continue + } + let hit = trimmed.matches(regex("^>HP:\\s*(\\d+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d+)\\s+(.+)$")).first() + let name = hit.captures.at(1) + if sequence != none and name != str(sequence) { + continue + } + let orientation = hit.captures.at(2) + let values = _strip-empty(hit.captures.at(4).split(" ")) + let spans = () + let idx = 0 + while idx + 1 < values.len() { + spans.push((int(values.at(idx)), int(values.at(idx + 1)))) + idx += 2 + } + return (name: name, orientation: orientation, spans: spans) + } + (name: "", orientation: "IN", spans: ()) +} + +#let read-phd-topology(source) = { + let runs = "" + for line in _split-lines(_source-text(source)) { + let matches = line.matches(regex("^\\s*PHDThtm\\s*\\|([^|]+)\\|")) + if matches.len() > 0 { + runs += matches.first().captures.at(0).replace(" ", "") + } + } + ( + topology: runs, + spans: _run-lengths(_chars(runs), "T"), + internal: _run-lengths(_chars(runs), "iI"), + external: _run-lengths(_chars(runs), "oO"), + ) +} + +#let read-phd-secondary(source) = { + let runs = "" + for line in _split-lines(_source-text(source)) { + let matches = line.matches(regex("^\\s*PHD sec\\s*\\|([^|]+)\\|")) + if matches.len() > 0 { + runs += matches.first().captures.at(0) + } + } + ( + sequence: runs, + alpha: _run-lengths(_chars(runs), "H"), + beta: _run-lengths(_chars(runs), "E"), + turn: _run-lengths(_chars(runs), "T"), + ) +} + +#let _runs-from-entries(entries, allowed) = { + let regions = () + let active = false + let start = 0 + let last = 0 + for entry in entries { + let pos = entry.at("pos") + let code = entry.at("code") + let hit = allowed.contains(code) + let contiguous = active and pos == last + 1 + if hit and (not active or not contiguous) { + if active { + regions.push((start, last)) + } + active = true + start = pos + } else if not hit and active { + regions.push((start, last)) + active = false + } + last = pos + } + if active { + regions.push((start, last)) + } + regions +} + +#let read-dssp(source, use-second-column: true) = { + let entries = () + let started = false + for line in _split-lines(_source-text(source)) { + if not started { + if line.contains("RESIDUE AA STRUCTURE") { + started = true + } + continue + } + if line.trim() == "" or line.len() < 17 { + continue + } + let first-field = line.slice(0, calc.min(5, line.len())).trim() + let second-field = line.slice(calc.min(5, line.len()), calc.min(10, line.len())).trim() + let aa = line.slice(calc.min(13, line.len()), calc.min(14, line.len())).trim() + if aa == "" or aa == "!" { + continue + } + let code = line.slice(calc.min(16, line.len()), calc.min(17, line.len())).trim() + let pos = if use-second-column and second-field != "" { + int(second-field) + } else if first-field != "" { + int(first-field) + } else { + entries.len() + 1 + } + entries.push((pos: pos, code: if code == "" { "C" } else { code })) + } + ( + sequence: entries.map(entry => entry.at("code")).join(""), + alpha: _runs-from-entries(entries, "H"), + "3-10": _runs-from-entries(entries, "G"), + pi: _runs-from-entries(entries, "I"), + beta: _runs-from-entries(entries, "E"), + bridge: _runs-from-entries(entries, "B"), + turn: _runs-from-entries(entries, "T"), + bend: _runs-from-entries(entries, "S"), + ) +} + +#let read-stride(source) = { + let entries = () + for line in _split-lines(_source-text(source)) { + let trimmed = line.trim() + if not trimmed.starts-with("ASG") { + continue + } + let hit = trimmed.matches(regex("^ASG\\s+\\S+\\s+\\S+\\s+(-?\\d+)\\s+(-?\\d+)\\s+([A-Z])")) + if hit.len() > 0 { + let captures = hit.first().captures + entries.push((pos: int(captures.at(1)), code: captures.at(2))) + continue + } + let fields = _strip-empty(trimmed.split(" ")) + if fields.len() >= 6 { + let pos = int(fields.at(4)) + let code = fields.at(5) + entries.push((pos: pos, code: code)) + } + } + ( + sequence: entries.map(entry => entry.at("code")).join(""), + alpha: _runs-from-entries(entries, "H"), + "3-10": _runs-from-entries(entries, "G"), + pi: _runs-from-entries(entries, "I"), + beta: _runs-from-entries(entries, "E"), + bridge: _runs-from-entries(entries, "B"), + turn: _runs-from-entries(entries, "T"), + ) +} diff --git a/packages/preview/typshade/0.1.3/internal/model/pdb.typ b/packages/preview/typshade/0.1.3/internal/model/pdb.typ new file mode 100644 index 0000000000..cd87131b4e --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/model/pdb.typ @@ -0,0 +1,210 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "parser.typ": _source-text, _split-lines, _upper + +#let _field(line, start, stop) = if line.len() >= stop { + line.slice(start, stop).trim() +} else { + "" +} + +#let _vec-sub(a, b) = ( + x: a.at("x") - b.at("x"), + y: a.at("y") - b.at("y"), + z: a.at("z") - b.at("z"), +) + +#let _vec-dot(a, b) = a.at("x") * b.at("x") + a.at("y") * b.at("y") + a.at("z") * b.at("z") +#let _vec-len2(a) = _vec-dot(a, a) + +#let _vec-cross(a, b) = ( + x: a.at("y") * b.at("z") - a.at("z") * b.at("y"), + y: a.at("z") * b.at("x") - a.at("x") * b.at("z"), + z: a.at("x") * b.at("y") - a.at("y") * b.at("x"), +) + +#let _read-pdb-atoms(source) = { + let atoms = () + for line in _split-lines(_source-text(source)) { + if not (line.starts-with("ATOM") or line.starts-with("HETATM")) or line.len() < 54 { + continue + } + let res = _field(line, 22, 26) + let x = _field(line, 30, 38) + let y = _field(line, 38, 46) + let z = _field(line, 46, 54) + if res == "" or x == "" or y == "" or z == "" { + continue + } + atoms.push(( + atom: _upper(_field(line, 12, 16)), + residue: int(res), + x: float(x), + y: float(y), + z: float(z), + )) + } + atoms +} + +#let _atoms-for-residue(atoms, residue) = { + let out = () + for atom in atoms { + if atom.at("residue") == residue { + out.push(atom) + } + } + out +} + +#let _atom-point(atom) = (x: atom.at("x"), y: atom.at("y"), z: atom.at("z")) + +#let _anchor-point(atoms, residue, atom-kind) = { + let residue-atoms = _atoms-for-residue(atoms, residue) + if residue-atoms.len() == 0 { + return none + } + let ca = none + for atom in residue-atoms { + if atom.at("atom") == "CA" { + ca = atom + } + } + if _upper(str(atom-kind)) == "CA" and ca != none { + return _atom-point(ca) + } + let backbone = ("N", "CA", "C", "O") + let best = none + let best-distance = -1.0 + let origin = if ca == none { residue-atoms.first() } else { ca } + for atom in residue-atoms { + if backbone.contains(atom.at("atom")) and residue-atoms.len() > backbone.len() { + continue + } + let distance = _vec-len2(_vec-sub(_atom-point(atom), _atom-point(origin))) + if distance > best-distance { + best = atom + best-distance = distance + } + } + if best == none { _atom-point(residue-atoms.first()) } else { _atom-point(best) } +} + +#let _parse-anchor(text) = { + let hit = str(text).trim().matches(regex("^(-?\\d+)(?:\\[([^\\]]+)\\])?$")) + if hit.len() == 0 { + return none + } + let captures = hit.first().captures + (residue: int(captures.at(0)), atom: captures.at(1, default: "side")) +} + +#let _parse-pdb-selection(selection) = { + if type(selection) == dictionary and selection.at("kind", default: none) == "pdb-selection" { + return selection + } + let hit = str(selection).trim().matches(regex("^(point|line|plane)(?:\\[([^\\]]+)\\])?:(.+)$")) + if hit.len() == 0 { + return none + } + let captures = hit.first().captures + let kind = captures.at(0) + let distance = if captures.at(1, default: "") == "" { 1.0 } else { float(captures.at(1)) } + let parts = captures.at(2).split(",").map(part => part.trim()).filter(part => part != "") + if parts.len() < 2 { + return none + } + let anchors = () + for part in parts.slice(1) { + let anchor = _parse-anchor(part) + if anchor != none { + anchors.push(anchor) + } + } + (kind: "pdb-selection", shape: kind, distance: distance, source: parts.first(), anchors: anchors) +} + +#let _distance2-to-segment(point, a, b) = { + let ab = _vec-sub(b, a) + let denom = _vec-len2(ab) + if denom == 0 { + return _vec-len2(_vec-sub(point, a)) + } + let t = calc.max(0.0, calc.min(1.0, _vec-dot(_vec-sub(point, a), ab) / denom)) + let projected = (x: a.at("x") + ab.at("x") * t, y: a.at("y") + ab.at("y") * t, z: a.at("z") + ab.at("z") * t) + _vec-len2(_vec-sub(point, projected)) +} + +#let _distance2-to-plane(point, a, b, c) = { + let normal = _vec-cross(_vec-sub(b, a), _vec-sub(c, a)) + let denom = _vec-len2(normal) + if denom == 0 { + return _distance2-to-segment(point, a, b) + } + let numerator = _vec-dot(_vec-sub(point, a), normal) + numerator * numerator / denom +} + +#let _residue-numbers(atoms) = { + let seen = (:) + let out = () + for atom in atoms { + let key = str(atom.at("residue")) + if not seen.keys().contains(key) { + seen.insert(key, true) + out.push(atom.at("residue")) + } + } + out.sorted() +} + +#let _pdb-selection-positions(selection) = { + let parsed = _parse-pdb-selection(selection) + if parsed == none { + return none + } + let atoms = _read-pdb-atoms(parsed.at("source")) + let anchors = () + for anchor in parsed.at("anchors") { + let point = _anchor-point(atoms, anchor.at("residue"), anchor.at("atom")) + if point != none { + anchors.push(point) + } + } + let shape = parsed.at("shape", default: parsed.at("kind")) + let required = if shape == "point" { 1 } else if shape == "line" { 2 } else { 3 } + if anchors.len() < required { + return () + } + let max-distance2 = parsed.at("distance") * parsed.at("distance") + let out = () + for residue in _residue-numbers(atoms) { + let selected = false + for atom in _atoms-for-residue(atoms, residue) { + let point = _atom-point(atom) + let distance2 = if shape == "point" { + _vec-len2(_vec-sub(point, anchors.at(0))) + } else if shape == "line" { + _distance2-to-segment(point, anchors.at(0), anchors.at(1)) + } else { + _distance2-to-plane(point, anchors.at(0), anchors.at(1), anchors.at(2)) + } + if distance2 <= max-distance2 { + selected = true + } + } + if selected { + out.push(residue) + } + } + out +} + +#let pdb-selection-list(selection) = { + let positions = _pdb-selection-positions(selection) + if positions == none { + return str(selection) + } + positions.map(pos => str(pos)).join(",") +} diff --git a/packages/preview/typshade/0.1.3/internal/model/text-style.typ b/packages/preview/typshade/0.1.3/internal/model/text-style.typ new file mode 100644 index 0000000000..806f2b51e2 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/model/text-style.typ @@ -0,0 +1,132 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "parser.typ": _upper + +#let _text-style-targets = ( + "residues", + "numbering", + "names", + "feature-text-labels", + "feature-style-labels", + "features", + "feature-styles", + "legend", + "ruler", + "ruler-label", +) + +#let _default-text-styles = ( + "residues": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "numbering": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "names": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "feature-text-labels": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "feature-style-labels": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "features": (family: "serif", weight: "regular", style: "italic", size: "normal"), + "feature-styles": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "legend": (family: "mono", weight: "regular", style: "normal", size: "normal"), + "ruler": (family: "sans", weight: "regular", style: "normal", size: "normal"), + "ruler-label": (family: "sans", weight: "regular", style: "normal", size: "normal"), +) + +#let _font-family(config, family) = { + if family == "mono" { + return config.at("font") + } + if family == "sans" { + return config.at("font-families").at("sans") + } + if family == "serif" { + return config.at("font-families").at("serif") + } + family +} + +#let _size-factor(size) = { + if type(size) != str { + return size + } + if size == "tiny" { + 0.55 + } else if size == "x-small" { + 0.7 + } else if size == "smaller" { + 0.8 + } else if size == "small" { + 0.9 + } else if size == "normal" { + 1.0 + } else if size == "large" { + 1.2 + } else if size == "x-large" { + 1.44 + } else if size == "xx-large" { + 1.72 + } else if size == "huge" { + 2.05 + } else if size == "x-huge" { + 2.45 + } else { + 1.0 + } +} + +#let _font-size(config, size) = { + if type(size) == str { + config.at("font-size") * _size-factor(size) + } else { + size + } +} + +#let _target-list(target) = if target == "all" { + _text-style-targets +} else if type(target) == str { + (target,) +} else { + target +} + +#let _set-text-style(config, target, key, value) = { + for name in _target-list(target) { + if config.at("text-styles").keys().contains(name) { + config.at("text-styles").at(name).insert(key, value) + } + } +} + +#let _text-style(config, target) = { + let base = _default-text-styles.at(target, default: _default-text-styles.at("residues")) + let overrides = config.at("text-styles").at(target, default: (:)) + let family = overrides.at("family", default: base.at("family")) + let weight = overrides.at("weight", default: base.at("weight")) + let style = overrides.at("style", default: base.at("style")) + let size = overrides.at("size", default: base.at("size")) + ( + font: _font-family(config, family), + size: _font-size(config, size), + weight: if weight == "bold" { "bold" } else { "regular" }, + style: if style == "italic" or style == "oblique" { "italic" } else { "normal" }, + smallcaps: style == "small-caps", + ) +} + +#let _text-params(config, target, fill: black, style: auto, size: auto) = { + let resolved = _text-style(config, target) + ( + font: resolved.at("font"), + size: if size == auto { resolved.at("size") } else { size }, + weight: resolved.at("weight"), + style: if style == auto { resolved.at("style") } else { style }, + fill: fill, + ) +} + +#let _text-string(config, target, value) = { + let text = str(value) + if _text-style(config, target).at("smallcaps") { + _upper(text) + } else { + text + } +} diff --git a/packages/preview/typshade/0.1.3/internal/render/alignment.typ b/packages/preview/typshade/0.1.3/internal/render/alignment.typ new file mode 100644 index 0000000000..1b261ba1a0 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/render/alignment.typ @@ -0,0 +1,1280 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/config.typ": _apply-command, _default-config +#import "features.typ": _bottom-feature-slots, _feature-rows, _ruler-rows, _top-feature-slots +#import "../engine/layout.typ": _empty-cell, _selection-columns, _sorted-unique +#import "../model/logo.typ": _resolve-sequence +#import "logos.typ": _legend-block, _logo-block +#import "../model/palette.typ": resolve-color, scale-color +#import "../model/parser.typ": _array-fill, _chars, _lower, _upper, read-alignment +#import "../model/text-style.typ": _font-size, _text-params, _text-string + +#let _display-columns(alignment, config) = { + let selected = () + for command in config.at("sequence-windows") { + let seq = alignment.at("sequences").at(_resolve-sequence(alignment, command.at("sequence"))) + for col in _selection-columns(seq, command.at("selection"), alignment: alignment) { + selected.push(col) + } + } + if selected.len() == 0 { + selected = range(0, alignment.at("columns")) + } + let columns = _sorted-unique(selected) + if not config.at("hide-allmatch-positions") { + return columns + } + let out = () + for col in columns { + let info = _style-for-column(alignment, config, col) + if info.at("consensus-score") < config.at("allmatch-threshold") { + out.push(col) + } + } + out +} + +#let _residue-group(char, seq-type, config) = { + let groups = if seq-type == "N" { config.at("dna-groups") } else { config.at("pep-groups") } + for (idx, group) in groups.enumerate() { + if group.contains(char) { + return idx + } + } + none +} + +#let _is-similar(residue, consensus, seq-type, config) = { + if residue == consensus { + return true + } + let sims = if seq-type == "N" { config.at("dna-sims") } else { config.at("pep-sims") } + if sims.keys().contains(consensus) { + return sims.at(consensus).contains(residue) + } + false +} + +#let _weight-order = ("C", "S", "T", "P", "A", "G", "N", "D", "E", "Q", "H", "R", "K", "M", "I", "L", "V", "F", "Y", "W") + +#let _weight-index(value) = { + for (idx, item) in _weight-order.enumerate() { + if item == value { + return idx + } + } + none +} + +#let _structural-rows = ( + C: (10, 6, 3, 3, 3, 5, 3, 1, 0, 1, 3, 3, 0, 3, 3, 3, 3, 5, 5, 5), + S: (6, 10, 8, 6, 8, 8, 8, 6, 5, 5, 5, 5, 5, 3, 3, 3, 6, 5, 5, 3), + T: (3, 8, 10, 6, 8, 6, 6, 5, 5, 5, 3, 5, 6, 5, 5, 3, 6, 3, 3, 1), + P: (3, 6, 6, 10, 8, 6, 3, 5, 5, 5, 5, 5, 3, 3, 3, 5, 6, 5, 3, 3), + A: (3, 8, 8, 8, 10, 8, 5, 6, 6, 5, 3, 3, 5, 5, 3, 3, 8, 5, 3, 3), + G: (5, 8, 6, 6, 8, 10, 5, 6, 6, 3, 1, 5, 3, 1, 3, 3, 6, 3, 3, 5), + N: (3, 8, 6, 3, 5, 5, 10, 8, 6, 5, 6, 5, 6, 1, 3, 1, 3, 3, 5, 0), + D: (1, 6, 5, 5, 6, 6, 8, 10, 8, 6, 5, 3, 5, 3, 1, 1, 5, 1, 3, 0), + E: (0, 5, 5, 5, 6, 6, 6, 8, 10, 6, 3, 5, 6, 3, 1, 1, 6, 3, 1, 1), + Q: (1, 5, 5, 5, 5, 3, 5, 6, 6, 10, 6, 5, 6, 3, 1, 3, 3, 1, 3, 1), + H: (3, 5, 3, 5, 3, 1, 6, 5, 3, 6, 10, 6, 5, 3, 3, 5, 1, 3, 5, 1), + R: (3, 5, 5, 5, 3, 5, 5, 3, 5, 5, 6, 10, 8, 3, 3, 3, 3, 1, 1, 3), + K: (0, 5, 6, 3, 5, 3, 6, 5, 6, 6, 5, 8, 10, 3, 3, 3, 5, 1, 1, 1), + M: (3, 5, 5, 3, 5, 1, 1, 3, 3, 3, 3, 3, 3, 10, 6, 8, 6, 5, 3, 5), + I: (3, 3, 5, 3, 3, 3, 3, 1, 1, 1, 3, 3, 3, 6, 10, 8, 3, 6, 5, 5), + L: (3, 3, 3, 5, 3, 3, 1, 1, 1, 3, 5, 3, 3, 8, 8, 10, 3, 6, 5, 6), + V: (3, 6, 6, 6, 8, 6, 3, 5, 6, 3, 1, 3, 5, 6, 3, 3, 10, 6, 5, 5), + F: (5, 5, 3, 5, 5, 3, 3, 1, 3, 1, 3, 1, 1, 5, 6, 6, 6, 10, 8, 5), + Y: (5, 5, 3, 3, 3, 3, 5, 3, 1, 3, 5, 1, 1, 3, 5, 5, 5, 8, 10, 5), + W: (5, 3, 1, 3, 3, 5, 0, 0, 1, 1, 1, 3, 1, 5, 5, 6, 5, 5, 5, 10), +) + +#let _pam250-rows = ( + C: (4, 0, -2, -3, -2, -3, -4, 2, -5, -5, -3, -4, -5, -5, -2, -6, -2, -4, 0, -8), + S: (0, 3, 1, 1, 1, 1, 1, 0, 0, -1, -1, 0, 0, -2, -1, -3, -1, -3, -3, -2), + T: (-2, 1, 3, 0, 1, 0, 0, 0, 0, -1, -1, -1, 0, -1, 0, -2, 0, -2, -3, -5), + P: (-3, 1, 0, 6, 1, -1, -1, -1, -1, 0, 0, 0, -1, -2, -2, -3, -1, -5, -5, -6), + A: (-2, 1, 1, 1, 2, 1, 0, 0, 0, 0, -1, -2, -1, -1, -1, -2, 0, -4, -3, -6), + G: (-3, 1, 0, -1, 1, 5, 0, 1, 0, -1, -2, -3, -2, -3, -3, -4, -1, -5, -5, -7), + N: (-4, 1, 0, -1, 0, 0, 2, 2, 1, 1, 2, 0, 1, -2, -2, -3, -2, -4, -2, -4), + D: (-5, 0, 0, -1, 0, 1, 2, 4, 3, 2, 1, -1, 0, -3, -2, -4, -2, -6, -4, -7), + E: (-5, 0, 0, -1, 0, 0, 1, 3, 4, 2, 1, -1, 0, -2, -2, -3, -2, -5, -4, -7), + Q: (-5, -1, -1, 0, 0, -1, 1, 2, 2, 4, 3, 1, 1, -1, -2, -2, -2, -5, -4, -5), + H: (-3, -1, -1, 0, -1, -2, 2, 1, 1, 3, 6, 2, 0, -2, -2, -2, -2, -2, -5, -7), + R: (-4, 0, -1, 0, -2, -3, 0, -1, -1, 1, 2, 6, 3, 0, -2, -3, -2, -4, -4, 2), + K: (-5, 0, 0, -1, -1, -2, 1, 0, 0, 1, 0, 3, 5, 0, -2, -3, -2, -5, -4, -3), + M: (-5, -2, -1, -2, -1, -3, -2, -3, -2, -1, -2, 0, 0, 6, 2, 4, 2, 0, -2, -4), + I: (-2, -1, 0, -2, -1, -3, -2, -2, -2, -2, -2, -2, -2, 2, 5, 2, 4, 1, -1, -5), + L: (-6, -3, -2, -3, -2, -4, -3, -4, -3, -2, -2, -3, -3, 4, 2, 6, 2, 2, -1, -2), + V: (-2, -1, 0, -1, 0, -1, -2, -2, -2, -2, -2, -2, -2, 2, 4, 2, 4, -1, -2, -6), + F: (-4, -3, -2, -5, -4, -5, -4, -6, -5, -5, -2, -4, -5, 0, 1, 2, -1, 9, 7, 0), + Y: (0, -3, -3, -5, -3, -5, -2, -4, -4, -4, 0, -4, -4, -2, -1, -1, -2, 7, 10, 0), + W: (-8, -2, -5, -6, -6, -7, -4, -7, -7, -5, -3, 2, -3, -4, -5, -2, -6, 0, 0, 17), +) + +#let _pam100-rows = ( + C: (14, -1, -5, -6, -5, -8, -8, -11, -11, -11, -6, -6, -11, -11, -5, -12, -4, -10, -2, -13), + S: (-1, 6, 2, 1, 2, 1, 2, -1, -2, -3, -4, -1, -2, -4, -4, -7, -4, -5, -6, -4), + T: (-5, 2, 7, -1, 2, -3, 0, -2, -3, -3, -5, -4, -1, -2, -1, -5, -1, -6, -6, -10), + P: (-6, 1, -1, 10, 1, -3, -3, -4, -3, -1, -2, -2, -4, -6, -6, -5, -4, -9, -11, -11), + A: (-5, 2, 2, 1, 6, 1, -1, -1, 0, -2, -5, -5, -4, -3, -3, -5, 0, -7, -6, -11), + G: (-8, 1, -3, -3, 1, 8, -1, -1, -2, -5, -7, -8, -5, -8, -7, -8, -4, -8, -11, -13), + N: (-8, 2, 0, -3, -1, -1, 7, 4, 1, -1, 2, -3, 1, -5, -4, -6, -5, -6, -3, -8), + D: (-11, -1, -2, -4, -1, -1, 4, 8, 5, 1, -1, -6, -2, -8, -6, -9, -6, -11, -9, -13), + E: (-11, -2, -3, -3, 0, -2, 1, 5, 8, 4, -2, -5, -2, -6, -5, -7, -5, -11, -7, -14), + Q: (-11, -3, -3, -1, -2, -5, -1, 1, 4, 9, 4, 1, -1, -2, -5, -3, -5, -10, -9, -11), + H: (-6, -4, -5, -2, -5, -7, 2, -1, -2, 4, 11, 1, -3, -7, -7, -5, -6, -4, -1, -7), + R: (-6, -1, -4, -2, -5, -8, -3, -6, -5, 1, 1, 10, 3, -2, -4, -7, -6, -7, -10, 1), + K: (-11, -2, -1, -4, -4, -5, 1, -2, -2, -1, -3, 3, 8, 1, -4, -6, -6, -11, -10, -9), + M: (-11, -4, -2, -6, -3, -8, -5, -8, -6, -2, -7, -2, 1, 13, 2, 4, 1, -2, -8, -11), + I: (-5, -4, -1, -6, -3, -7, -4, -6, -5, -5, -7, -4, -4, 2, 9, 2, 5, 0, -4, -12), + L: (-12, -7, -5, -5, -5, -8, -6, -9, -7, -3, -5, -7, -6, 4, 2, 9, 1, 0, -5, -7), + V: (-4, -4, -1, -4, 0, -4, -5, -6, -5, -5, -6, -6, -6, 1, 5, 1, 8, -5, -6, -14), + F: (-10, -5, -6, -9, -7, -8, -6, -11, -11, -10, -4, -7, -11, -2, 0, 0, -5, 12, 6, -2), + Y: (-2, -6, -6, -11, -6, -11, -3, -9, -7, -9, -1, -10, -10, -8, -4, -5, -6, 6, 13, -2), + W: (-13, -4, -10, -11, -11, -13, -8, -13, -14, -11, -7, 1, -9, -11, -12, -7, -14, -2, -2, 19), +) + +#let _blosum62-rows = ( + C: (9, -1, -1, -3, 0, -3, -3, -3, -4, -3, -3, -3, -3, -1, -1, -1, -1, -2, -2, -2), + S: (-1, 4, 1, -1, 1, 0, 1, 0, 0, 0, -1, -1, 0, -1, -2, -2, -2, -2, -2, -3), + T: (-1, 1, 4, 1, -1, 1, 0, 1, 0, 0, 0, -1, 0, -1, -2, -2, -2, -2, -2, -3), + P: (-3, -1, 1, 7, -1, -2, -1, -1, -1, -1, -2, -2, -1, -2, -3, -3, -2, -4, -3, -4), + A: (0, 1, -1, -1, 4, 0, -1, -2, -1, -1, -2, -1, -1, -1, -1, -1, -2, -2, -2, -3), + G: (-3, 0, 1, -2, 0, 6, -2, -1, -2, -2, -2, -2, -2, -3, -4, -4, 0, -3, -3, -2), + N: (-3, 1, 0, -2, -2, 0, 6, 1, 0, 0, -1, 0, 0, -2, -3, -3, -3, -3, -2, -4), + D: (-3, 0, 1, -1, -2, -1, 1, 6, 2, 0, -1, -2, -1, -3, -3, -4, -3, -3, -3, -4), + E: (-4, 0, 0, -1, -1, -2, 0, 2, 5, 2, 0, 0, 1, -2, -3, -3, -3, -3, -2, -3), + Q: (-3, 0, 0, -1, -1, -2, 0, 0, 2, 5, 0, 1, 1, 0, -3, -2, -2, -3, -1, -2), + H: (-3, -1, 0, -2, -2, -2, 1, 1, 0, 0, 8, 0, -1, -2, -3, -3, -2, -1, 2, -2), + R: (-3, -1, -1, -2, -1, -2, 0, -2, 0, 1, 0, 5, 2, -1, -3, -2, -3, -3, -2, -3), + K: (-3, 0, 0, -1, -1, -2, 0, -1, 1, 1, -1, 2, 5, -1, -3, -2, -3, -3, -2, -3), + M: (-1, -1, -1, -2, -1, -3, -2, -3, -2, 0, -2, -1, -1, 5, 1, 2, -2, 0, -1, -1), + I: (-1, -2, -2, -3, -1, -4, -3, -3, -3, -3, -3, -3, -3, 1, 4, 2, 1, 0, -1, -3), + L: (-1, -2, -2, -3, -1, -4, -3, -4, -3, -2, -3, -2, -2, 2, 2, 4, 3, 0, -1, -2), + V: (-1, -2, -2, -2, 0, -3, -3, -3, -2, -2, -3, -3, -2, 1, 3, 1, 4, -1, -1, -3), + F: (-2, -2, -2, -4, -2, -3, -3, -3, -3, -3, -1, -3, -3, 0, 0, 0, -1, 6, 3, 1), + Y: (-2, -2, -2, -3, -2, -3, -2, -3, -2, -1, 2, -2, -2, -1, -1, -1, -1, 3, 7, 2), + W: (-2, -3, -3, -4, -3, -2, -4, -4, -3, -2, -2, -3, -3, -1, -3, -2, -3, 1, 2, 11), +) + +#let _matrix-score(rows, a, b) = { + let row = rows.at(a, default: none) + let index = _weight-index(b) + if row == none or index == none { + none + } else { + row.at(index) + } +} + +#let _residue-weight(candidate, residue, seq-type, config) = { + let pair = _upper(candidate) + ":" + _upper(residue) + let custom = config.at("custom-weights") + if custom.keys().contains(pair) { + return custom.at(pair) + } + if str(config.at("weight-table")) == "structural" { + let score = _matrix-score(_structural-rows, _upper(candidate), _upper(residue)) + if score != none { + return score + } + } + if str(config.at("weight-table")) == "BLOSUM62" { + let score = _matrix-score(_blosum62-rows, _upper(candidate), _upper(residue)) + if score != none { + return score + } + } + if str(config.at("weight-table")) == "PAM250" { + let score = _matrix-score(_pam250-rows, _upper(candidate), _upper(residue)) + if score != none { + return score + } + } + if str(config.at("weight-table")) == "PAM100" { + let score = _matrix-score(_pam100-rows, _upper(candidate), _upper(residue)) + if score != none { + return score + } + } + if candidate == residue { + return 10 + } + let table = str(config.at("weight-table")) + if table == "identity" { + return 0 + } + if _is-similar(candidate, residue, seq-type, config) { + return if table == "structural" { 6 } else { 4 } + } + if _residue-group(candidate, seq-type, config) == _residue-group(residue, seq-type, config) { + return if table == "structural" { 3 } else { 1 } + } + if table == "PAM250" or table == "PAM100" or table == "BLOSUM62" { + return -2 + } + 0 +} + +#let _weighted-consensus(counts, residues, seq-type, config) = { + if counts.keys().len() == 0 { + return (consensus: none, score: 0) + } + let best = counts.keys().first() + let best-score = none + let best-count = 0 + for candidate in counts.keys() { + let score = 0 + for residue in residues { + let key = _upper(residue) + if key == "." or key == "-" or key == "" { + score += config.at("gap-penalty") + } else { + score += _residue-weight(candidate, key, seq-type, config) + } + } + if best-score == none or score > best-score or (score == best-score and counts.at(candidate) > best-count) { + best = candidate + best-score = score + best-count = counts.at(candidate) + } + } + let max-self = calc.max(1, _residue-weight(best, best, seq-type, config)) + let normalized = calc.max(0, calc.min(100, best-score / (max-self * residues.len()) * 100)) + (consensus: best, score: normalized) +} + +#let _matches-sequence(alignment, refs, index) = { + let seq = alignment.at("sequences").at(index) + for ref in refs { + if ref == index + 1 or str(ref) == str(index + 1) or str(ref) == seq.at("name") { + return true + } + } + false +} + +#let _validate-sequence-ref-list(alignment, refs) = { + for ref in refs { + let _ = _resolve-sequence(alignment, ref) + } + none +} + +#let _validate-sequence-ref-map(alignment, refs) = { + for ref in refs.keys() { + let _ = _resolve-sequence(alignment, ref) + } + none +} + +#let _validate-config-sequence-refs(alignment, config) = { + _validate-sequence-ref-list(alignment, config.at("hidden-names")) + _validate-sequence-ref-list(alignment, config.at("hidden-numbers")) + _validate-sequence-ref-list(alignment, config.at("hidden")) + _validate-sequence-ref-list(alignment, config.at("killed")) + _validate-sequence-ref-list(alignment, config.at("no-shade")) + _validate-sequence-ref-list(alignment, config.at("separation-lines")) + _validate-sequence-ref-list(alignment, config.at("subfamily")) + _validate-sequence-ref-map(alignment, config.at("sequence-names")) + _validate-sequence-ref-map(alignment, config.at("sequence-name-colors")) + _validate-sequence-ref-map(alignment, config.at("sequence-number-colors")) + _validate-sequence-ref-map(alignment, config.at("start-numbers")) + _validate-sequence-ref-map(alignment, config.at("sequence-lengths")) + none +} + +#let _sequence-label(alignment, config, sequence, sequence-index) = { + let by-index = config.at("sequence-names").at(str(sequence-index + 1), default: none) + if by-index != none { + return by-index + } + config.at("sequence-names").at(sequence.at("name"), default: sequence.at("name")) +} + +#let _sequence-color(config, sequence, sequence-index, key, default-color) = { + let colors = config.at(key) + let by-index = colors.at(str(sequence-index + 1), default: none) + if by-index != none { + return by-index + } + colors.at(sequence.at("name"), default: default-color) +} + +#let _config-ref-value(map, sequence, sequence-index, default: none) = { + let by-index = map.at(str(sequence-index + 1), default: none) + if by-index != none { + return by-index + } + map.at(sequence.at("name"), default: default) +} + +#let _renumber-position(raw-pos, start, allow-zero) = { + if raw-pos == none { + return none + } + let shifted = start + raw-pos - 1 + if not allow-zero and start < 1 and shifted >= 0 { + shifted + 1 + } else { + shifted + } +} + +#let _rebuild-sequence(sequence, aligned) = { + let positions = () + let raw = "" + let pos = 0 + for ch in _chars(aligned) { + if ch == "." or ch == "-" { + positions.push(none) + } else { + pos += 1 + raw += ch + positions.push(pos) + } + } + sequence.insert("aligned", aligned) + sequence.insert("raw", raw) + sequence.insert("positions", positions) + sequence.insert("length", pos) +} + +#let _apply-single-sequence-options(alignment, config) = { + if alignment.at("sequences").len() != 1 or config.at("single-seq-shift") == none { + return + } + let sequence = alignment.at("sequences").first() + let aligned = sequence.at("aligned") + if not config.at("keep-single-seq-gaps") { + while aligned.starts-with(".") or aligned.starts-with("-") { + aligned = aligned.slice(1) + if aligned.len() == 0 { + break + } + } + } + let shift = int(config.at("single-seq-shift")) + if shift > 0 { + aligned = "." * shift + aligned + } else if shift < 0 { + let remove = calc.min(-shift, aligned.len()) + aligned = aligned.slice(remove) + } + _rebuild-sequence(sequence, aligned) + alignment.insert("columns", aligned.len()) +} + +#let _apply-numbering-overrides(alignment, config) = { + for (idx, sequence) in alignment.at("sequences").enumerate() { + let start = _config-ref-value(config.at("start-numbers"), sequence, idx, default: 1) + if start != 1 { + let positions = () + for pos in sequence.at("positions") { + positions.push(_renumber-position(pos, start, config.at("allow-zero"))) + } + sequence.insert("positions", positions) + } + let length = _config-ref-value(config.at("sequence-lengths"), sequence, idx, default: none) + if length != none { + sequence.insert("length", length) + } + } +} + +#let _functional-mode(config) = { + let option = config.at("shading").at("option", default: none) + if option == none { config.at("functional-default") } else { option } +} + +#let _functional-style(config, residue) = { + let overrides = config.at("functional-style-overrides") + if overrides.keys().contains(residue) { + return overrides.at(residue) + } + let mode = _functional-mode(config) + let groups = config.at("functional-groups") + if not groups.keys().contains(mode) { + return none + } + for group in groups.at(mode) { + if group.at("residues").contains(residue) { + return group + } + } + none +} + +#let _tint-color(effect) = { + if effect == "weak" { + "Gray10" + } else if effect == "strong" { + "Gray30" + } else { + "LightGray" + } +} + +#let _column-tcoffee-score(alignment, config, col) = { + let total = 0 + let count = 0 + for seq in alignment.at("sequences") { + let scores = config.at("tcoffee").at("scores").at(seq.at("name"), default: ()) + if col < scores.len() and scores.at(col) != none { + total += scores.at(col) + count += 1 + } + } + if count == 0 { 0 } else { total / count } +} + +#let _style-for-column(alignment, config, col) = { + let mode = config.at("shading").at("mode") + let seq-type = alignment.at("seq-type") + let residues = () + let counts = (:) + for seq in alignment.at("sequences") { + let char = seq.at("aligned").slice(col, col + 1) + residues.push(char) + if char != "." and char != "-" { + let key = _upper(char) + counts.insert(key, counts.at(key, default: 0) + 1) + } + } + let styles = () + let consensus = none + let consensus-score = 0 + let consensus-forced = false + if counts.keys().len() > 0 { + let max-key = counts.keys().first() + let max-count = counts.at(max-key) + for key in counts.keys() { + if counts.at(key) > max-count { + max-key = key + max-count = counts.at(key) + } + } + consensus = max-key + consensus-score = max-count / alignment.at("sequences").len() * 100 + } + let consensus-source = config.at("consensus").at("source", default: "all") + if consensus-source == "all" and (config.at("weight-table") != "identity" or config.at("custom-weights").keys().len() > 0 or config.at("gap-penalty") != 0) { + let weighted = _weighted-consensus(counts, residues, seq-type, config) + consensus = weighted.at("consensus") + consensus-score = weighted.at("score") + } + if consensus-source != "all" { + let source-index = _resolve-sequence(alignment, consensus-source) + let source-char = _upper(alignment.at("sequences").at(source-index).at("aligned").slice(col, col + 1)) + if source-char == "." or source-char == "-" or source-char == "" { + consensus = none + consensus-score = 0 + } else { + consensus = source-char + consensus-score = counts.at(source-char, default: 0) / alignment.at("sequences").len() * 100 + consensus-forced = true + } + } + let group-majority = none + let group-counts = (:) + for key in counts.keys() { + let group = _residue-group(key, seq-type, config) + if group != none { + let group-key = str(group) + group-counts.insert(group-key, group-counts.at(group-key, default: 0) + counts.at(key)) + } + } + let max-group-count = 0 + for key in group-counts.keys() { + if group-counts.at(key) > max-group-count { + max-group-count = group-counts.at(key) + group-majority = int(key) + } + } + let reference-index = if config.at("shading").at("reference") != none { _resolve-sequence(alignment, config.at("shading").at("reference")) } else { 0 } + let reference-char = alignment.at("sequences").at(reference-index).at("aligned").slice(col, col + 1) + let styled = (kind, char) => { + let style = config.at("residue-style").at(kind) + let token = style.at("case", default: "upper") + let source-char = if char == "*" { config.at("stop-char") } else { char } + let out-char = if token == "upper" { + _upper(source-char) + } else if token == "lower" { + _lower(source-char) + } else if token == "" or token == "normal" { + source-char + } else { + token + } + ( + kind: kind, + char: out-char, + fg: style.at("fg"), + bg: style.at("bg"), + emph: style.at("style", default: "normal") == "italic" or style.at("style", default: "normal") == "oblique", + ) + } + for (res-index, char) in residues.enumerate() { + if char == "." or char == "-" { + styles.push((kind: "gap", char: config.at("gaps").at("char"), fg: config.at("gaps").at("fg"), bg: config.at("gaps").at("bg"), rule: config.at("gaps").at("char") == "rule")) + } else if mode == "T-Coffee" { + let seq-name = alignment.at("sequences").at(res-index).at("name") + let seq-scores = config.at("tcoffee").at("scores").at(seq-name, default: ()) + let score = if col < seq-scores.len() and seq-scores.at(col) != none { seq-scores.at(col) } else { 0 } + styles.push((kind: "tcoffee", char: char, fg: "Black", bg: scale-color("TCoffee", score), score: score)) + } else if mode == "functional" { + let group = _functional-style(config, _upper(char)) + if group == none { + styles.push(styled("nomatch", char)) + } else { + let styled-char = styled("nomatch", char).at("char") + let token = group.at("case", default: "upper") + let out-char = if token == "upper" { + _upper(char) + } else if token == "lower" { + _lower(char) + } else if token == "" or token == "normal" { + char + } else { + token + } + styles.push(( + kind: "functional", + char: if group.keys().contains("case") { out-char } else { styled-char }, + fg: group.at("fg"), + bg: group.at("bg"), + emph: group.at("style", default: "normal") == "italic" or group.at("style", default: "normal") == "oblique", + )) + } + } else if mode == "diverse" { + if char == reference-char { + styles.push((kind: "allmatch", char: ".", fg: "Black", bg: "White")) + } else if _is-similar(_upper(char), _upper(reference-char), seq-type, config) { + styles.push((kind: "conserved", char: ".", fg: "Black", bg: "White")) + } else { + styles.push((kind: "nomatch", char: _lower(char), fg: "Black", bg: "White")) + } + } else if mode == "similar" { + if consensus != none and consensus-score >= config.at("allmatch-threshold") { + styles.push(styled("allmatch", char)) + } else if consensus != none and consensus-score >= config.at("threshold") and char == consensus { + styles.push(styled("conserved", char)) + } else if group-majority != none and max-group-count / alignment.at("sequences").len() * 100 >= config.at("threshold") and _residue-group(_upper(char), seq-type, config) == group-majority { + styles.push(styled("similar", char)) + } else { + styles.push(styled("nomatch", char)) + } + } else if mode == "singleseq" { + styles.push(styled("nomatch", char)) + } else { + if consensus != none and consensus-score >= config.at("allmatch-threshold") { + styles.push(styled("allmatch", char)) + } else if consensus != none and consensus-score >= config.at("threshold") and char == consensus { + styles.push(styled("conserved", char)) + } else { + styles.push(styled("nomatch", char)) + } + } + } + (styles: styles, consensus: consensus, consensus-score: consensus-score, consensus-forced: consensus-forced) +} + +#let _base-cell(char, fg: "Black", bg: "White", lower: false, emph: false, frame: none) = ( + char: if lower { _lower(char) } else { char }, + fg: fg, + bg: bg, + emph: emph, + frame: frame, +) + +#let _merge-cell-delta(cell, delta) = { + if delta == none { + return cell + } + assert(type(delta) == dictionary, message: "typshade: cell-style callbacks must return none or a dictionary") + let updated = cell + for key in ("char", "fg", "bg", "emph", "frame", "frame-thickness", "rule") { + if delta.keys().contains(key) { + updated.insert(key, delta.at(key)) + } + } + if delta.at("case", default: none) == "upper" { + updated.insert("char", _upper(updated.at("char"))) + } else if delta.at("case", default: none) == "lower" { + updated.insert("char", _lower(updated.at("char"))) + } + if delta.at("lower", default: false) { + updated.insert("char", _lower(updated.at("char"))) + } + updated +} + +#let _apply-cell-styles(alignment, config, sequence, sequence-index, column, column-info, style-info, cell) = { + let updated = cell + if config.at("cell-styles").len() == 0 { + return updated + } + let cell-context = ( + alignment: alignment, + sequence: sequence, + sequence-index: sequence-index, + sequence-number: sequence-index + 1, + sequence-name: sequence.at("name"), + column-index: column, + column: column + 1, + position: sequence.at("positions").at(column), + residue: sequence.at("aligned").slice(column, column + 1), + kind: style-info.at("kind", default: "custom"), + consensus: column-info.at("consensus"), + consensus-score: column-info.at("consensus-score"), + consensus-forced: column-info.at("consensus-forced", default: false), + cell: updated, + ) + for callback in config.at("cell-styles") { + updated = _merge-cell-delta(updated, callback(cell-context)) + cell-context.insert("cell", updated) + } + updated +} + +#let _apply-regions(alignment, config, sequence-index, column, cell) = { + let updated = cell + for region in config.at("shade-regions") { + let target = _resolve-sequence(alignment, region.at("sequence")) + if target == sequence-index or region.at("all") { + let seq = alignment.at("sequences").at(target) + if _selection-columns(seq, region.at("selection"), alignment: alignment).contains(column) { + let explicit-bg = region.at("bg", default: none) + let scheme = region.at("scheme", default: config.at("shading").at("scheme")) + let fill = if explicit-bg != none { explicit-bg } else if scheme == "reds" { "BrickRed" } else if scheme == "greens" { "PineGreen" } else if scheme == "grays" { "DarkGray" } else { "RoyalBlue" } + updated.insert("bg", fill) + updated.insert("fg", if region.at("fg", default: none) != none { region.at("fg") } else if fill == "DarkGray" or fill == "PineGreen" or fill == "RoyalBlue" { "White" } else { updated.at("fg") }) + } + } + } + for region in config.at("tint-regions") { + let target = _resolve-sequence(alignment, region.at("sequence")) + if target == sequence-index { + let seq = alignment.at("sequences").at(target) + if _selection-columns(seq, region.at("selection"), alignment: alignment).contains(column) { + let effect = if region.at("intensity", default: "medium") == "medium" { config.at("tint-default") } else { region.at("intensity") } + updated.insert("bg", _tint-color(effect)) + } + } + } + for region in config.at("lower-regions") { + let target = _resolve-sequence(alignment, region.at("sequence")) + if target == sequence-index { + let seq = alignment.at("sequences").at(target) + if _selection-columns(seq, region.at("selection"), alignment: alignment).contains(column) { + updated.insert("char", _lower(updated.at("char"))) + } + } + } + for region in config.at("emph-regions") { + let target = _resolve-sequence(alignment, region.at("sequence")) + if target == sequence-index { + let seq = alignment.at("sequences").at(target) + if _selection-columns(seq, region.at("selection"), alignment: alignment).contains(column) { + let style = if region.at("style", default: "italic") == "italic" { config.at("emph-default") } else { region.at("style") } + updated.insert("emph", style != "normal") + } + } + } + for region in config.at("frame-regions") { + let target = _resolve-sequence(alignment, region.at("sequence")) + if target == sequence-index { + let seq = alignment.at("sequences").at(target) + if _selection-columns(seq, region.at("selection"), alignment: alignment).contains(column) { + updated.insert("frame", region.at("color")) + } + } + } + updated +} + +#let _visible-sequences(alignment, config) = { + if config.at("hide-seqs") { + return () + } + let out = () + for seq in alignment.at("sequences") { + let idx = _resolve-sequence(alignment, seq.at("name")) + 1 + let hidden = config.at("hidden").contains(idx) or config.at("hidden").contains(seq.at("name")) + let killed = config.at("killed").contains(idx) or config.at("killed").contains(seq.at("name")) + if not hidden and not killed { + out.push(seq) + } + } + if config.at("shading").at("mode") == "singleseq" { + let ref = config.at("shading").at("reference", default: 1) + return (alignment.at("sequences").at(_resolve-sequence(alignment, ref)),) + } + if config.at("order") == none { + return out + } + let ordered = () + for ref in config.at("order") { + let idx = _resolve-sequence(alignment, ref) + ordered.push(alignment.at("sequences").at(idx)) + } + ordered +} + +#let _consensus-row(alignment, config, columns, segment) = { + let cells = () + for col in segment { + let info = _style-for-column(alignment, config, col) + let score = info.at("consensus-score") + let level = if info.at("consensus") == none { + "none" + } else if info.at("consensus-forced", default: false) and score < config.at("threshold") { + "conserved" + } else if score >= config.at("allmatch-threshold") { + "allmatch" + } else if score >= config.at("threshold") { + "conserved" + } else { + "none" + } + let char = if level == "none" { + config.at("consensus").at("symbols").at("none") + } else if level == "allmatch" { + let token = config.at("consensus").at("symbols").at("allmatch") + if token == "upper" { + _upper(info.at("consensus")) + } else if token == "lower" { + _lower(info.at("consensus")) + } else { + token + } + } else if level == "conserved" { + let token = config.at("consensus").at("symbols").at("conserved") + if token == "upper" { + _upper(info.at("consensus")) + } else if token == "lower" { + _lower(info.at("consensus")) + } else { + token + } + } else { + config.at("consensus").at("symbols").at("none") + } + let color-style = config.at("consensus").at("colors").at(level) + let bg = if config.at("consensus").at("scale") == none { + color-style.at("bg") + } else { + let scale-score = if config.at("consensus").at("scale") == "T-Coffee" { _column-tcoffee-score(alignment, config, col) } else { score } + scale-color(config.at("consensus").at("scale"), scale-score) + } + cells.push((char: char, fg: color-style.at("fg"), bg: bg, emph: false, frame: none)) + } + (label: config.at("consensus").at("name"), left: "", right: "", cells: cells, row-kind: "consensus") +} + + +#let _row-for-sequence(alignment, config, sequence, sequence-index, segment) = { + let cells = () + let no-shade = _matches-sequence(alignment, config.at("no-shade"), sequence-index) + for col in segment { + let column-info = _style-for-column(alignment, config, col) + let style-info = column-info.at("styles").at(sequence-index) + if no-shade { + let raw-char = sequence.at("aligned").slice(col, col + 1) + if raw-char == "." or raw-char == "-" { + style-info = (char: config.at("gaps").at("char"), fg: config.at("gaps").at("fg"), bg: config.at("gaps").at("bg"), emph: false, rule: config.at("gaps").at("char") == "rule") + } else { + style-info = (char: if raw-char == "*" { config.at("stop-char") } else { raw-char }, fg: "Black", bg: "White", emph: false) + } + } + let cell = (char: style-info.at("char"), fg: style-info.at("fg"), bg: style-info.at("bg"), emph: style-info.at("emph", default: false), frame: none, rule: style-info.at("rule", default: false)) + cell = _apply-cell-styles(alignment, config, sequence, sequence-index, col, column-info, style-info, cell) + cells.push(_apply-regions(alignment, config, sequence-index, col, cell)) + } + let hide-number = _matches-sequence(alignment, config.at("hidden-numbers"), sequence-index) + let left-num = if config.at("numbering").at("show") and config.at("numbering").at("left") { + let pos = none + for col in segment { + if sequence.at("positions").at(col) != none { + pos = sequence.at("positions").at(col) + break + } + } + if pos == none or hide-number { "" } else { str(pos) } + } else { "" } + let right-num = if config.at("numbering").at("show") and config.at("numbering").at("right") { + let pos = none + let pos-col = none + for col in segment.rev() { + if sequence.at("positions").at(col) != none { + pos = sequence.at("positions").at(col) + pos-col = col + break + } + } + if pos != none and sequence.at("length", default: none) != none { + let last-col = none + for (idx, mapped) in sequence.at("positions").enumerate() { + if mapped != none { + last-col = idx + } + } + if pos-col == last-col { + pos = sequence.at("length") + } + } + if pos == none or hide-number { "" } else { str(pos) } + } else { "" } + let label = if _matches-sequence(alignment, config.at("hidden-names"), sequence-index) { "" } else { _sequence-label(alignment, config, sequence, sequence-index) } + ( + label: label, + label-color: _sequence-color(config, sequence, sequence-index, "sequence-name-colors", config.at("names-color")), + number-color: _sequence-color(config, sequence, sequence-index, "sequence-number-colors", config.at("numbering-color")), + left: left-num, + right: right-num, + cells: cells, + row-kind: "sequence", + ) +} + +#let _separation-row(config, segment) = ( + label: "", + left: "", + right: "", + cells: _array-fill(segment.len(), _empty-cell()), + row-kind: "separation", + height: config.at("separation-space"), +) + +#let _row-label-target(row) = { + let kind = row.at("row-kind") + if kind == "feature-text" { + "feature-text-labels" + } else if kind == "feature" { + "feature-style-labels" + } else if kind == "ruler" { + "ruler-label" + } else { + "names" + } +} + +#let _row-cell-target(row) = { + let kind = row.at("row-kind") + if kind == "feature-text" { + "features" + } else if kind == "feature" { + "featurestyles" + } else if kind == "ruler" { + "ruler" + } else { + "residues" + } +} + +#let _name-width(alignment, config) = { + if not config.at("names").at("show") { + return 0pt + } + let widths = (0pt,) + for seq in alignment.at("sequences") { + let idx = _resolve-sequence(alignment, seq.at("name")) + widths.push(measure(text(.._text-params(config, "names"))[#_text-string(config, "names", _sequence-label(alignment, config, seq, idx))]).width) + } + if config.at("consensus").at("show") and config.at("consensus").at("name") != "" { + widths.push(measure(text(.._text-params(config, "names"))[#_text-string(config, "names", config.at("consensus").at("name"))]).width) + } + if config.at("sequence-logo").at("show") { + widths.push(measure(text(.._text-params(config, "names"))[#_text-string(config, "names", config.at("sequence-logo").at("name"))]).width) + } + if config.at("subfamily-logo").at("show") { + widths.push(measure(text(.._text-params(config, "names"))[#_text-string(config, "names", config.at("subfamily-logo").at("name"))]).width) + widths.push(measure(text(.._text-params(config, "names"))[#_text-string(config, "names", config.at("subfamily-logo").at("negative-name"))]).width) + } + for label in config.at("feature-style-names").values() { + widths.push(measure(text(.._text-params(config, "feature-style-labels"))[#_text-string(config, "feature-style-labels", label)]).width) + } + for label in config.at("feature-text-names").values() { + widths.push(measure(text(.._text-params(config, "feature-text-labels"))[#_text-string(config, "feature-text-labels", label)]).width) + } + for label in config.at("ruler-names").values() { + widths.push(measure(text(.._text-params(config, "ruler-label"))[#_text-string(config, "ruler-label", label)]).width) + } + calc.max(..widths) + 6pt +} + +#let _render-table(rows, config, name-width, num-width, cell-width) = { + let fills = () + let strokes = () + let items = () + let columns = () + if config.at("names").at("show") and config.at("names").at("position") == "left" { + columns.push(name-width) + } + if config.at("numbering").at("show") and config.at("numbering").at("left") { + columns.push(num-width) + } + let residue-cols = rows.first().at("cells").len() + for _ in range(0, residue-cols) { + columns.push(cell-width) + } + if config.at("numbering").at("show") and config.at("numbering").at("right") { + columns.push(num-width) + } + if config.at("names").at("show") and config.at("names").at("position") == "right" { + columns.push(name-width) + } + for row in rows { + let row-fills = () + let row-strokes = () + let label-color = row.at("label-color", default: "Black") + let number-color = row.at("number-color", default: config.at("numbering-color")) + let label-target = _row-label-target(row) + let cell-target = _row-cell-target(row) + let hide-sequence-chars = config.at("hide-residues") and row.at("row-kind") == "sequence" + if config.at("names").at("show") and config.at("names").at("position") == "left" { + row-fills.push(none) + row-strokes.push(none) + items.push(align(if config.at("align-right-labels") { right } else { left }, text(.._text-params(config, label-target, fill: resolve-color(label-color)))[#_text-string(config, label-target, row.at("label"))])) + } + if config.at("numbering").at("show") and config.at("numbering").at("left") { + row-fills.push(none) + row-strokes.push(none) + items.push(align(right, text(.._text-params(config, "numbering", fill: resolve-color(number-color)))[#_text-string(config, "numbering", row.at("left"))])) + } + for cell in row.at("cells") { + row-fills.push(if cell.at("bg") == none { none } else { resolve-color(cell.at("bg")) }) + row-strokes.push(if cell.at("frame") == none { none } else { (paint: resolve-color(cell.at("frame")), thickness: cell.at("frame-thickness", default: 0.5pt)) }) + if cell.at("rule", default: false) { + items.push(box(width: 100%, height: config.at("font-size"), inset: 0pt)[ + #align(horizon, rect(width: 100%, height: config.at("gaps").at("rule-thickness"), fill: resolve-color(cell.at("fg")), stroke: none)) + ]) + } else if row.at("row-kind") == "separation" { + items.push(box(width: 100%, height: row.at("height"), inset: 0pt)[]) + } else if cell.keys().contains("rotated") { + items.push(box(width: 100%, height: config.at("font-size") * 3, inset: 0pt)[ + #align(center + horizon, rotate(-90deg, reflow: false)[ + #text(.._text-params(config, cell-target, fill: resolve-color(cell.at("fg"))))[#_text-string(config, cell-target, cell.at("rotated"))] + ]) + ]) + } else if cell.keys().contains("ruler-label") { + items.push(box(width: 100%, inset: 0pt)[ + #align(center + horizon)[ + #text(.._text-params(config, cell-target, fill: resolve-color(cell.at("fg"))))[#_text-string(config, cell-target, cell.at("ruler-label"))] + ] + ]) + } else { + items.push(text(.._text-params(config, cell-target, fill: resolve-color(cell.at("fg")), style: if cell.at("emph") { "italic" } else { auto }, size: if cell.keys().contains("size") { _font-size(config, cell.at("size")) } else { auto }))[#{ + if hide-sequence-chars or (cell.at("char") == "." and not config.at("show-leading-gaps")) { "" } else { _text-string(config, cell-target, cell.at("char")) } + }]) + } + } + if config.at("numbering").at("show") and config.at("numbering").at("right") { + row-fills.push(none) + row-strokes.push(none) + items.push(align(left, text(.._text-params(config, "numbering", fill: resolve-color(number-color)))[#_text-string(config, "numbering", row.at("right"))])) + } + if config.at("names").at("show") and config.at("names").at("position") == "right" { + row-fills.push(none) + row-strokes.push(none) + items.push(align(if config.at("align-right-labels") { left } else { right }, text(.._text-params(config, label-target, fill: resolve-color(label-color)))[#_text-string(config, label-target, row.at("label"))])) + } + fills.push(row-fills) + strokes.push(row-strokes) + } + let table-width = 0pt + for column in columns { + table-width += column + } + box(width: table-width, inset: 0pt)[ + #table(columns: columns, inset: (x: 2pt, y: 1pt), stroke: (x, y) => strokes.at(y).at(x), fill: (x, y) => fills.at(y).at(x), align: center, column-gutter: 0pt, row-gutter: config.at("line-gap"), ..items) + ] +} + +#let _append-feature-blocks(rendered, alignment, config, segment, slots, name-width, num-width, cell-width) = { + for slot in slots { + let rows = _feature-rows(alignment, config, segment, slot) + if rows.len() > 0 { + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + let spacing = config.at("feature-slot-spacing").at(slot) + if spacing != 0pt { + rendered.push(v(spacing, weak: false)) + } + } + } +} + +#let _alignment-edge(config) = { + let value = config.at("alignment") + if value == "left" { + left + } else if value == "right" { + right + } else { + center + } +} + +#let _line-count-is-auto(value) = value == auto or str(value) == "auto" + +#let _label-number-width(config, name-width, num-width) = { + let fixed = 0pt + if config.at("names").at("show") { + fixed += name-width + } + if config.at("numbering").at("show") and config.at("numbering").at("left") { + fixed += num-width + } + if config.at("numbering").at("show") and config.at("numbering").at("right") { + fixed += num-width + } + fixed +} + +#let _resolved-residues-per-line(config, display-columns, available-width, cell-width, name-width, num-width) = { + let requested = config.at("residues-per-line") + if not _line-count-is-auto(requested) { + return calc.max(1, int(requested)) + } + let limits = config.at("auto-layout") + let lower = calc.max(1, int(limits.at("min", default: 1))) + let upper = limits.at("max", default: none) + let clamp = value => { + let bounded = calc.max(lower, value) + if upper == none { + bounded + } else { + calc.min(int(upper), bounded) + } + } + if available-width == none or available-width == auto { + return clamp(calc.max(1, calc.min(60, display-columns.len()))) + } + if available-width > 10000pt { + return clamp(calc.max(1, calc.min(60, display-columns.len()))) + } + let fixed = _label-number-width(config, name-width, num-width) + let usable = calc.max(cell-width, available-width - fixed - 6pt) + clamp(calc.max(1, calc.min(display-columns.len(), int(calc.floor(usable / cell-width))))) +} + +#let _resolved-blocks-per-page(alignment, config, available-height) = { + let page = config.at("auto-page") + if not page.at("enabled") { + return none + } + let requested = page.at("blocks") + if requested != auto and requested != none { + return calc.max(1, int(requested)) + } + if requested == none or available-height == none or available-height == auto or available-height > 10000pt { + return none + } + let sequence-rows = _visible-sequences(alignment, config).len() + let consensus-rows = if config.at("consensus").at("show") { 1 } else { 0 } + let ruler-rows = if config.at("ruler").at("show") { 2 } else { 0 } + let logo-rows = (if config.at("sequence-logo").at("show") { 3 } else { 0 }) + (if config.at("subfamily-logo").at("show") and config.at("subfamily").len() > 0 { 3 } else { 0 }) + let feature-count = config.at("features").len() + config.at("structure-data").len() + let feature-rows = calc.min(6, feature-count) + let rows = calc.max(1, sequence-rows + consensus-rows + ruler-rows + logo-rows + feature-rows) + let row-height = config.at("font-size") + config.at("line-gap") + 2pt + let estimated = rows * row-height + config.at("block-gap") + calc.max(1, int(calc.floor(available-height / estimated))) +} + +#let _legend-style-label(kind) = { + if kind == "allmatch" { + "all match" + } else if kind == "conserved" { + "conserved" + } else if kind == "similar" { + "similar" + } else if kind == "gap" { + "gap" + } else if kind == "tcoffee" { + "T-Coffee" + } else { + kind + } +} + +#let _legend-key-part(value) = { + if value == none { + "none" + } else if type(value) == color { + "color" + } else { + str(value) + } +} + +#let _legend-items(alignment, config, columns) = { + if not config.at("legend").at("show") or config.at("shading").at("mode") == "functional" { + return () + } + let items = (:) + let wanted = ("similar", "conserved", "allmatch", "gap", "tcoffee") + for col in columns { + let info = _style-for-column(alignment, config, col) + for style in info.at("styles") { + let kind = style.at("kind", default: "nomatch") + if wanted.contains(kind) { + let key = kind + ":" + _legend-key-part(style.at("fg", default: none)) + ":" + _legend-key-part(style.at("bg", default: none)) + if not items.keys().contains(key) { + items.insert(key, ( + label: _legend-style-label(kind), + fg: style.at("fg", default: "Black"), + bg: style.at("bg", default: "White"), + )) + } + } + } + } + if config.at("cell-styles").len() > 0 and not items.keys().contains("cell-style") { + items.insert("cell-style", (label: "cell-style", fg: "Black", bg: "Gray10")) + } + items.values() +} + +#let _render-block-stack(config, edge, rendered) = { + let aligned = rendered.map(item => align(edge)[#item]) + if config.at("fixed-block-space") { + return stack(spacing: config.at("block-gap"), ..aligned) + } + let body = [] + for (idx, item) in aligned.enumerate() { + if idx > 0 and config.at("block-gap") != 0pt { + body += v(config.at("block-gap"), weak: true) + } + body += item + } + body +} + +#let render-alignment(source, format: auto, commands: (), font: none, font-size: none, residues-per-line: none) = { + context { + let config = _default-config() + if font != none { + config.insert("font", font) + } + if font-size != none { + config.insert("font-size", font-size) + } + if residues-per-line != none { + config.insert("residues-per-line", residues-per-line) + } + for command in commands { + config = _apply-command(config, command) + } + let alignment = read-alignment(source, format: format) + if config.at("seq-type") != none { + alignment.insert("seq-type", config.at("seq-type")) + } + let _ = _validate-config-sequence-refs(alignment, config) + _apply-single-sequence-options(alignment, config) + _apply-numbering-overrides(alignment, config) + let render = (available-width, available-height) => { + let sequences = _visible-sequences(alignment, config) + let display-columns = _display-columns(alignment, config) + let cell-width = (measure(text(.._text-params(config, "residues"))[M]).width + 1.5pt) * config.at("char-stretch") + let name-width = _name-width(alignment, config) + let num-sample = "9" * config.at("numbering-width-digits") + let num-width = measure(text(.._text-params(config, "numbering"))[#num-sample]).width + 4pt + let blocks = () + let step = _resolved-residues-per-line(config, display-columns, available-width, cell-width, name-width, num-width) + let start = 0 + while start < display-columns.len() { + let stop = calc.min(start + step, display-columns.len()) + blocks.push(display-columns.slice(start, stop)) + start = stop + } + let rendered = () + if config.at("captions").at("top") != none { + rendered.push(text(.._text-params(config, "legend"))[#config.at("captions").at("top")]) + } + let legend = _legend-block(config, items: _legend-items(alignment, config, display-columns)) + if legend != none { + rendered.push(legend) + } + let blocks-per-page = _resolved-blocks-per-page(alignment, config, available-height) + for (segment-index, segment) in blocks.enumerate() { + _append-feature-blocks(rendered, alignment, config, segment, _top-feature-slots, name-width, num-width, cell-width) + let rows = () + if config.at("ruler").at("show") and config.at("ruler").at("position") == "top" { + for row in _ruler-rows(alignment, config, segment, "top") { + rows.push(row) + } + if config.at("ruler-spacing").at("top") != 0pt { + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + rendered.push(v(config.at("ruler-spacing").at("top"), weak: false)) + rows = () + } + } + if config.at("sequence-logo").at("show") and config.at("sequence-logo").at("position") == "top" { + rendered.push(_logo-block(alignment, config, segment, name-width, num-width, cell-width)) + } + if config.at("subfamily-logo").at("show") and config.at("subfamily-logo").at("position") == "top" and config.at("subfamily").len() > 0 { + rendered.push(_logo-block(alignment, config, segment, name-width, num-width, cell-width, subfamily: true)) + } + if config.at("consensus").at("show") and config.at("consensus").at("position") == "top" { + rows.push(_consensus-row(alignment, config, display-columns, segment)) + } + for seq in sequences { + let seq-index = _resolve-sequence(alignment, seq.at("name")) + rows.push(_row-for-sequence(alignment, config, seq, seq-index, segment)) + if _matches-sequence(alignment, config.at("separation-lines"), seq-index) { + rows.push(_separation-row(config, segment)) + } + } + if config.at("consensus").at("show") and config.at("consensus").at("position") == "bottom" { + rows.push(_consensus-row(alignment, config, display-columns, segment)) + } + if config.at("sequence-logo").at("show") and config.at("sequence-logo").at("position") == "bottom" { + rows.push((label: "", left: "", right: "", cells: _array-fill(segment.len(), _empty-cell()), row-kind: "spacer")) + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + rendered.push(_logo-block(alignment, config, segment, name-width, num-width, cell-width)) + rows = () + } + if config.at("subfamily-logo").at("show") and config.at("subfamily-logo").at("position") == "bottom" and config.at("subfamily").len() > 0 { + if rows.len() > 0 { + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + rows = () + } + rendered.push(_logo-block(alignment, config, segment, name-width, num-width, cell-width, subfamily: true)) + } + if config.at("ruler").at("show") and config.at("ruler").at("position") == "bottom" { + if rows.len() > 0 and config.at("ruler-spacing").at("bottom") != 0pt { + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + rendered.push(v(config.at("ruler-spacing").at("bottom"), weak: false)) + rows = () + } + for row in _ruler-rows(alignment, config, segment, "bottom") { + rows.push(row) + } + } + if rows.len() > 0 { + rendered.push(_render-table(rows, config, name-width, num-width, cell-width)) + } + _append-feature-blocks(rendered, alignment, config, segment, _bottom-feature-slots, name-width, num-width, cell-width) + if blocks-per-page != none and segment-index + 1 < blocks.len() and calc.rem(segment-index + 1, blocks-per-page) == 0 { + rendered.push(pagebreak(weak: true)) + if legend != none and config.at("auto-page").at("repeat-legend") { + rendered.push(legend) + } + } + } + if config.at("captions").at("bottom") != none { + rendered.push(text(.._text-params(config, "legend"))[#config.at("captions").at("bottom")]) + } + let edge = _alignment-edge(config) + _render-block-stack(config, edge, rendered) + } + if _line-count-is-auto(config.at("residues-per-line")) or config.at("auto-page").at("blocks") == auto { + layout(size => render(size.width, size.height)) + } else { + render(none, none) + } + } +} diff --git a/packages/preview/typshade/0.1.3/internal/render/features.typ b/packages/preview/typshade/0.1.3/internal/render/features.typ new file mode 100644 index 0000000000..9c329c9b5e --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/render/features.typ @@ -0,0 +1,388 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "graphs.typ": _graph-rows, _graph-style +#import "../engine/layout.typ": _empty-cell, _ordinal-label, _selection-columns, _selection-string +#import "../model/logo.typ": _resolve-sequence +#import "../model/parser.typ": _array-fill, _chars, _lower, _upper + +#let _top-feature-slots = ("ttttop", "tttop", "ttop", "top") +#let _bottom-feature-slots = ("bottom", "bbottom", "bbbottom", "bbbbottom") + +#let _array-position(items, value) = { + for (idx, item) in items.enumerate() { + if item == value { + return idx + } + } + none +} + +#let _feature-label(config, position, text: false) = { + let labels = if text { config.at("feature-text-names") } else { config.at("feature-style-names") } + let colors = if text { config.at("feature-text-name-colors") } else { config.at("feature-style-name-colors") } + ( + text: labels.at(position, default: ""), + color: colors.at(position, default: colors.at("default")), + ) +} + +#let _roman(index, upper: false) = { + let pairs = ((1000, "m"), (900, "cm"), (500, "d"), (400, "cd"), (100, "c"), (90, "xc"), (50, "l"), (40, "xl"), (10, "x"), (9, "ix"), (5, "v"), (4, "iv"), (1, "i")) + let value = index + let out = "" + for pair in pairs { + while value >= pair.at(0) { + out += pair.at(1) + value -= pair.at(0) + } + } + if upper { _upper(out) } else { out } +} + +#let _structure-template(text, number, letter) = { + let lower-letter = _lower(letter) + str(text) + .replace("\\numcount", str(number)) + .replace("\\alphacount", lower-letter) + .replace("\\Alphacount", letter) + .replace("\\romancount", _roman(number)) + .replace("\\Romancount", _roman(number, upper: true)) + .replace("$\\alpha$", "α") + .replace("$\\beta$", "β") + .replace("$\\pi$", "π") + .replace("$\\circ$", "○") + .replace("$\\uparrow$", "↑") + .replace("$\\diamond$", "◇") + .replace("$_{10}$", "10") + .replace("$", "") +} + +#let _structure-items(alignment, config) = { + let items = () + for entry in config.at("structure-data") { + let prefix = entry.at("kind") + let sequence = entry.at("sequence") + let shown = config.at("structure-show").at(prefix, default: ()) + let data = entry.at("data") + if prefix == "HMMTOP" or prefix == "PHDtopo" { + for (idx, range) in data.at("internal", default: ()).enumerate() { + if shown.contains("internal") { + let appearance = config.at("structure-appearance").at(prefix + ":internal") + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + for (idx, range) in data.at("external", default: ()).enumerate() { + if shown.contains("external") { + let appearance = config.at("structure-appearance").at(prefix + ":external") + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + for (idx, range) in data.at("spans", default: data.at("tm", default: ())).enumerate() { + if shown.contains("TM") { + let appearance = config.at("structure-appearance").at(prefix + ":TM") + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + } else if prefix == "PHDsec" { + for (idx, range) in data.at("alpha", default: ()).enumerate() { + if shown.contains("alpha") { + let appearance = config.at("structure-appearance").at("PHDsec:alpha") + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + for (idx, range) in data.at("beta", default: ()).enumerate() { + if shown.contains("beta") { + let appearance = config.at("structure-appearance").at("PHDsec:beta") + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + } else if prefix == "STRIDE" or prefix == "DSSP" { + for key in shown { + for (idx, range) in data.at(key, default: ()).enumerate() { + let appearance = config.at("structure-appearance").at(prefix + ":" + key) + let letter = _ordinal-label(idx + 1) + items.push((position: appearance.at("position"), sequence: sequence, selection: _selection-string(range), style: _structure-template(appearance.at("style"), idx + 1, letter), text: _structure-template(appearance.at("text"), idx + 1, letter))) + } + } + } + } + items +} + +#let _base-matches(pattern, base) = { + let map = ( + A: "A", C: "C", G: "G", T: "TU", U: "TU", + R: "AG", Y: "CTU", K: "GTU", M: "AC", S: "CG", W: "ATU", + B: "CGTU", D: "AGTU", H: "ACTU", V: "ACG", N: "ACGTU", + ) + map.at(_upper(pattern), default: _upper(pattern)).contains(_upper(base)) +} + +#let _codon-matches(pattern, codon) = { + if pattern.len() != 3 or codon.len() != 3 { + return false + } + for idx in range(0, 3) { + if not _base-matches(pattern.slice(idx, idx + 1), codon.slice(idx, idx + 1)) { + return false + } + } + true +} + +#let _translate-codon(config, codon) = { + let cleaned = _upper(str(codon)).replace("U", "T") + for residue in config.at("genetic-code").keys() { + if _codon-matches(config.at("genetic-code").at(residue).replace("U", "T"), cleaned) { + return residue + } + } + "X" +} + +#let _complement-base(base) = { + let pairs = (A: "T", T: "A", U: "A", G: "C", C: "G", R: "Y", Y: "R", K: "M", M: "K", S: "S", W: "W", B: "V", V: "B", D: "H", H: "D", N: "N") + pairs.at(_upper(base), default: _upper(base)) +} + +#let _style-colors(style, default-fg: "Black", default-bg: none) = { + let hit = str(style).matches(regex("\\[([^\\]]+)\\]")) + if hit.len() == 0 { + return (fg: default-fg, bg: default-bg) + } + let parts = hit.first().captures.at(0).split(",").map(part => part.trim()) + (fg: if parts.len() > 0 and parts.first() != "" { parts.first() } else { default-fg }, bg: if parts.len() > 1 and parts.at(1) != "" { parts.at(1) } else { default-bg }) +} + +#let _backtranslation-label(config, triplet, index) = { + let style = config.at("backtranslation").at("label-style") + if style == "vertical" { + triplet.slice(0, 1) + "\n" + triplet.slice(1, 2) + "\n" + triplet.slice(2, 3) + } else if style == "oblique" { + triplet.slice(0, 1) + "/" + triplet.slice(1, 2) + "/" + triplet.slice(2, 3) + } else if style == "zigzag" { + if calc.rem(index, 2) == 0 { triplet } else { triplet.slice(2, 3) + triplet.slice(1, 2) + triplet.slice(0, 1) } + } else { + triplet + } +} + +#let _translation-cells(alignment, config, sequence, style, segment, selected) = { + let cells = _array-fill(segment.len(), _empty-cell()) + let colors = _style-colors(style) + if alignment.at("seq-type") == "P" { + for (idx, col) in segment.enumerate() { + if selected.contains(col) { + let residue = _upper(sequence.at("aligned").slice(col, col + 1)) + if residue != "." and residue != "-" { + cells.at(idx).insert("char", _backtranslation-label(config, config.at("genetic-code").at(residue, default: "NNN"), idx)) + cells.at(idx).insert("fg", colors.at("fg")) + cells.at(idx).insert("bg", colors.at("bg")) + cells.at(idx).insert("size", config.at("backtranslation").at("label-size")) + } + } + } + return cells + } + let codon = "" + let codon-cols = () + for col in selected { + let residue = sequence.at("aligned").slice(col, col + 1) + if residue == "." or residue == "-" { + continue + } + codon += residue + codon-cols.push(col) + if codon.len() == 3 { + let center-col = codon-cols.at(1) + let target = _array-position(segment, center-col) + if target != none { + cells.at(target).insert("char", _translate-codon(config, codon)) + cells.at(target).insert("fg", colors.at("fg")) + cells.at(target).insert("bg", colors.at("bg")) + cells.at(target).insert("size", config.at("backtranslation").at("text-size")) + } + codon = "" + codon-cols = () + } + } + cells +} + +#let _complement-cells(config, sequence, style, segment, selected) = { + let cells = _array-fill(segment.len(), _empty-cell()) + let colors = _style-colors(style) + let lower = str(style).contains("[lower]") + for (idx, col) in segment.enumerate() { + if selected.contains(col) { + let residue = sequence.at("aligned").slice(col, col + 1) + if residue != "." and residue != "-" { + let base = _complement-base(residue) + cells.at(idx).insert("char", if lower { _lower(base) } else { base }) + cells.at(idx).insert("fg", colors.at("fg")) + cells.at(idx).insert("bg", colors.at("bg")) + } + } + } + cells +} + +#let _feature-cell(config, style, col, selected) = { + if not selected.contains(col) { + return _empty-cell() + } + if style == "" { + return (char: "•", fg: "Black", bg: "LightGray", emph: false, frame: none) + } + if style.starts-with("box") { + let fill = "LightGray" + let hit = style.matches(regex("^box\\[([^\\]]+)\\]")) + if hit.len() > 0 { + fill = hit.first().captures.at(0) + } + return (char: "", fg: "Black", bg: fill, emph: false, frame: "Black", frame-thickness: config.at("feature-rule")) + } + if style == "helix" { + return (char: "≋", fg: "Black", bg: none, emph: false, frame: none) + } + if style.starts-with("fill:") { + let symbol = style.split(":").last() + return (char: if symbol.len() > 0 { symbol.slice(0, 1) } else { "•" }, fg: "Black", bg: none, emph: false, frame: none) + } + if style.starts-with("brace") { + return (char: "⌒", fg: "Black", bg: none, emph: false, frame: none) + } + if style.contains("=") { + return (char: "━", fg: "Black", bg: none, emph: false, frame: none) + } + if style.contains("-") or style.contains(">") or style.contains("<") or style.contains(",") { + return (char: "─", fg: "Black", bg: none, emph: false, frame: none) + } + (char: style.slice(0, 1), fg: "Black", bg: "LightGray", emph: false, frame: none) +} + +#let _ruler-rows(alignment, config, segment, position) = { + let seq = alignment.at("sequences").at(_resolve-sequence(alignment, config.at("ruler").at("sequence"))) + let steps = config.at("ruler").at("steps") + let ruler-color = config.at("ruler-colors").at(position, default: config.at("ruler").at("color")) + let ruler-name = config.at("ruler-names").at(position, default: "") + let ruler-name-color = config.at("ruler-name-colors").at(position, default: ruler-color) + let alt-labels = config.at("ruler-labels").at(position, default: (:)) + let rotated = config.at("ruler-rotation").at(position, default: false) + let ticks = () + let labels = _array-fill(segment.len(), _empty-cell()) + for col in segment { + let pos = seq.at("positions").at(col) + if pos != none and calc.rem(pos, steps) == 0 { + ticks.push((char: "|", fg: ruler-color, bg: none, emph: false, frame: none)) + } else { + ticks.push(_empty-cell()) + } + } + for (idx, col) in segment.enumerate() { + let pos = seq.at("positions").at(col) + if pos != none and calc.rem(pos, steps) == 0 { + let override = alt-labels.at(str(pos), default: none) + let text = if override == none { str(pos) } else { override.at("text") } + let color = if override == none or override.at("color") == none { ruler-color } else { override.at("color") } + if rotated { + labels.at(idx).insert("char", "") + labels.at(idx).insert("rotated", text) + labels.at(idx).insert("fg", color) + } else { + labels.at(idx).insert("char", "") + labels.at(idx).insert("ruler-label", text) + labels.at(idx).insert("fg", color) + } + } + } + let label-row = (label: ruler-name, label-color: ruler-name-color, left: "", right: "", cells: labels, row-kind: "ruler") + let tick-row = (label: "", left: "", right: "", cells: ticks, row-kind: "ruler") + if position == "bottom" { + (tick-row, label-row) + } else { + (label-row, tick-row) + } +} + +#let _feature-rows(alignment, config, segment, position) = { + let rows = () + let entries = () + for item in config.at("features") { + entries.push(item) + } + for item in _structure-items(alignment, config) { + entries.push(item) + } + for item in entries { + if item.at("position") != position { + continue + } + let seq = alignment.at("sequences").at(_resolve-sequence(alignment, item.at("sequence"))) + let selected = _selection-columns(seq, item.at("selection"), alignment: alignment) + let graph = _graph-style(item.at("style")) + let cells = if graph != none { + none + } else if str(item.at("style")).starts-with("translate") { + _translation-cells(alignment, config, seq, item.at("style"), segment, selected) + } else if str(item.at("style")).starts-with("complement") { + _complement-cells(config, seq, item.at("style"), segment, selected) + } else { + let out = () + for col in segment { + out.push(_feature-cell(config, item.at("style"), col, selected)) + } + out + } + if graph != none { + let style-label = _feature-label(config, position) + for (row-index, graph-row) in _graph-rows(alignment, config, seq, selected, segment, item.at("style"), position: position).enumerate() { + rows.push(( + label: if row-index == 0 { style-label.at("text") } else { "" }, + label-color: style-label.at("color"), + left: "", + right: "", + cells: graph-row, + row-kind: "feature", + )) + } + } else { + let style-label = _feature-label(config, position) + rows.push((label: style-label.at("text"), label-color: style-label.at("color"), left: "", right: "", cells: cells, row-kind: "feature")) + } + let style-text = if type(item.at("style")) == str { item.at("style") } else { "" } + let inline-text = if style-text.starts-with("box") and style-text.contains(":") { + style-text.split(":").last() + } else { + "" + } + let label = if item.at("text") != "" { item.at("text") } else { inline-text } + if label != "" { + let text-label = _feature-label(config, position, text: true) + let label-cells = _array-fill(segment.len(), _empty-cell()) + let in-segment = () + for (idx, col) in segment.enumerate() { + if selected.contains(col) { + in-segment.push(idx) + } + } + if in-segment.len() > 0 { + let start = calc.max(0, calc.min(in-segment.first(), in-segment.last()) - 1) + for (offset, ch) in _chars(label).enumerate() { + let target = start + offset + if target < label-cells.len() { + label-cells.at(target).insert("char", ch) + } + } + } + rows.push((label: text-label.at("text"), label-color: text-label.at("color"), left: "", right: "", cells: label-cells, row-kind: "feature-text")) + } + } + rows +} diff --git a/packages/preview/typshade/0.1.3/internal/render/graphs.typ b/packages/preview/typshade/0.1.3/internal/render/graphs.typ new file mode 100644 index 0000000000..cdd9388e31 --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/render/graphs.typ @@ -0,0 +1,626 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../engine/layout.typ": _empty-cell +#import "../model/palette.typ": resolve-color, scale-color +#import "../model/parser.typ": _source-is-path, _source-text, _split-lines, _upper + +#let _hydropathy-scale = ( + A: 1.8, C: 2.5, D: -3.5, E: -3.5, F: 2.8, G: -0.4, H: -3.2, I: 4.5, + K: -3.9, L: 3.8, M: 1.9, N: -3.5, P: -1.6, Q: -3.5, R: -4.5, S: -0.8, + T: -0.7, V: 4.2, W: -0.9, Y: -1.3, +) + +#let _molweight-scale = ( + A: 89.1, C: 121.2, D: 133.1, E: 147.1, F: 165.2, G: 75.1, H: 155.2, I: 131.2, + K: 146.2, L: 131.2, M: 149.2, N: 132.1, P: 115.1, Q: 146.1, R: 174.2, S: 105.1, + T: 119.1, V: 117.1, W: 204.2, Y: 181.2, +) + +#let _charge-scale = ( + D: -1.0, E: -1.0, K: 1.0, R: 1.0, H: 0.5, +) + +#let _graph-scales = ("BlackWhite", "WhiteBlack", "BlueRed", "RedBlue", "GreenRed", "RedGreen", "ColdHot", "HotCold", "TCoffee") +#let _builtin-graph-metrics = ("hydrophobicity", "molweight", "charge", "conservation", "entropy", "gap-fraction", "gaps", "coverage", "identity", "identity-to-reference") + +#let _array-position(items, value) = { + for (idx, item) in items.enumerate() { + if item == value { + return idx + } + } + none +} + +#let _graph-is-stacked(parsed) = parsed.at("kind") == "stackedbars" or parsed.at("kind") == "frustratometer" + +#let _parse-number(value) = { + let text = str(value).trim() + if text == "" or text == "NaN" or text == "nan" { + none + } else { + float(text) + } +} + +#let _graph-style(style) = { + if type(style) == dictionary { + if not style.keys().contains("kind") or not style.keys().contains("metric") { + return none + } + return ( + kind: style.at("kind"), + metric: style.at("metric"), + min: style.at("min", default: none), + max: style.at("max", default: none), + options: style.at("options", default: ()), + ) + } + let text = str(style).trim() + let hit = text.matches(regex("^(bar|color|stackedbars|frustratometer)(?:\\[([^\\]]*)\\])?:(.+?)(?:\\[([^\\]]*)\\])?$")) + if hit.len() == 0 { + return none + } + let captures = hit.first().captures + let kind = captures.at(0) + let range-text = captures.at(1, default: "") + let metric = captures.at(2).trim() + let option-text = captures.at(3, default: "") + let min = none + let max = none + if range-text != "" { + let parts = range-text.split(",") + if parts.len() >= 2 { + min = _parse-number(parts.at(0)) + max = _parse-number(parts.at(1)) + } + } + let options = if option-text == "" { () } else { option-text.split(",").map(part => part.trim()) } + (kind: kind, metric: metric, min: min, max: max, options: options) +} + +#let _graph-range(metric) = if metric == "hydrophobicity" { + (-4.5, 4.5) +} else if metric == "molweight" { + (75.1, 204.2) +} else if metric == "charge" { + (-1.0, 1.0) +} else { + (0.0, 100.0) +} + +#let _graph-range-from-style(parsed, series: none) = { + if parsed.at("min") != none and parsed.at("max") != none { + return (parsed.at("min"), parsed.at("max")) + } + if parsed.at("kind") == "frustratometer" { + return (0.0, 1.0) + } + if _builtin-graph-metrics.contains(parsed.at("metric")) { + return _graph-range(parsed.at("metric")) + } + let min = none + let max = none + for item in series { + let values = if type(item) == array or type(item) == content { item } else { (item,) } + for value in values { + if value == none { + continue + } + if min == none or value < min { + min = value + } + if max == none or value > max { + max = value + } + } + } + if min == none or max == none { + (0.0, 1.0) + } else if min == max { + (min, max + 1.0) + } else { + (min, max) + } +} + +#let _graph-column-counts(alignment, col) = { + let counts = (:) + let total = 0 + let gaps = 0 + for seq in alignment.at("sequences") { + let residue = _upper(seq.at("aligned").slice(col, col + 1)) + if residue == "." or residue == "-" or residue == "" { + gaps += 1 + continue + } + counts.insert(residue, counts.at(residue, default: 0) + 1) + total += 1 + } + (counts: counts, total: total, gaps: gaps) +} + +#let _graph-conservation(alignment, col) = { + let data = _graph-column-counts(alignment, col) + let counts = data.at("counts") + let total = data.at("total") + if total == 0 { + return 0.0 + } + let top = 0 + for key in counts.keys() { + if counts.at(key) > top { + top = counts.at(key) + } + } + top / total * 100.0 +} + +#let _graph-entropy(alignment, col) = { + let data = _graph-column-counts(alignment, col) + let counts = data.at("counts") + let total = data.at("total") + if total == 0 { + return 0.0 + } + let entropy = 0.0 + for key in counts.keys() { + let p = counts.at(key) / total + if p > 0 { + entropy += -p * calc.log(p) / calc.log(2) + } + } + let max-entropy = if alignment.at("seq-type") == "N" { 2.0 } else { calc.log(20.0) / calc.log(2) } + calc.max(0.0, calc.min(100.0, entropy / max-entropy * 100.0)) +} + +#let _graph-gap-fraction(alignment, col) = _graph-column-counts(alignment, col).at("gaps") / alignment.at("sequences").len() * 100.0 + +#let _graph-coverage(alignment, col) = _graph-column-counts(alignment, col).at("total") / alignment.at("sequences").len() * 100.0 + +#let _graph-identity(alignment, sequence, col) = { + let residue = _upper(sequence.at("aligned").slice(col, col + 1)) + if residue == "." or residue == "-" or residue == "" { + return none + } + let total = 0 + let hits = 0 + for seq in alignment.at("sequences") { + let other = _upper(seq.at("aligned").slice(col, col + 1)) + if other == "." or other == "-" or other == "" { + continue + } + total += 1 + if other == residue { + hits += 1 + } + } + if total == 0 { none } else { hits / total * 100.0 } +} + +#let _builtin-graph-value(alignment, sequence, col, metric) = { + let residue = _upper(sequence.at("aligned").slice(col, col + 1)) + if (metric == "hydrophobicity" or metric == "molweight" or metric == "charge") and (residue == "." or residue == "-" or residue == "") { + return none + } + if metric == "hydrophobicity" { + return _hydropathy-scale.at(residue, default: 0.0) + } + if metric == "molweight" { + return _molweight-scale.at(residue, default: 100.0) + } + if metric == "charge" { + return _charge-scale.at(residue, default: 0.0) + } + if metric == "conservation" { + return _graph-conservation(alignment, col) + } + if metric == "entropy" { + return _graph-entropy(alignment, col) + } + if metric == "gap-fraction" or metric == "gaps" { + return _graph-gap-fraction(alignment, col) + } + if metric == "coverage" { + return _graph-coverage(alignment, col) + } + if metric == "identity" or metric == "identity-to-reference" { + return _graph-identity(alignment, sequence, col) + } + none +} + +#let _parse-inline-graph-data(text, stacked: false) = { + let source = str(text).trim() + if stacked { + let rows = () + for hit in source.matches(regex("\\(([^\\)]*)\\)")) { + let values = () + for part in hit.captures.at(0).split(",") { + values.push(_parse-number(part)) + } + rows.push((position: none, values: values)) + } + return rows + } + let values = () + for part in source.split(",") { + values.push(_parse-number(part)) + } + values.map(value => (position: none, values: (value,))) +} + +#let _parse-graph-line(line, stacked: false) = { + let trimmed = str(line).trim() + if trimmed == "" { + return none + } + let first = trimmed.slice(0, 1) + if first.matches(regex("[A-Za-z]")).len() > 0 and not trimmed.starts-with("NaN") and not trimmed.starts-with("nan") { + return none + } + let position = none + let body = trimmed + let numbered = trimmed.matches(regex("^(-?\\d+)\\s*:\\s*(.+)$")) + if numbered.len() > 0 { + position = int(numbered.first().captures.at(0)) + body = numbered.first().captures.at(1) + } + let values = () + for part in body.split(",") { + values.push(_parse-number(part)) + } + if not stacked and values.len() > 1 { + values = (values.first(),) + } + (position: position, values: values) +} + +#let _read-graph-data(source, stacked: false) = { + let rows = () + for line in _split-lines(_source-text(source)) { + let parsed = _parse-graph-line(line, stacked: stacked) + if parsed != none { + rows.push(parsed) + } + } + rows +} + +#let _read-frustr-data(source) = { + let rows = () + for line in _split-lines(_source-text(source)) { + let trimmed = str(line).trim() + if trimmed == "" or trimmed.starts-with("#") { + continue + } + let parts = trimmed.split().filter(part => part != "") + if parts.len() < 9 { + continue + } + let residue = _parse-number(parts.at(0)) + if residue == none { + continue + } + let rel-high = _parse-number(parts.at(6)) + let rel-neutral = _parse-number(parts.at(7)) + let rel-min = _parse-number(parts.at(8)) + rows.push((position: int(residue), values: (rel-min, rel-neutral, rel-high))) + } + rows +} + +#let _graph-data-source(parsed) = { + let metric = parsed.at("metric") + if type(metric) == str and _builtin-graph-metrics.contains(metric) { + return "builtin" + } + if type(metric) == bytes or _source-is-path(metric) { + return if parsed.at("kind") == "frustratometer" { "frustr" } else { "source" } + } + if parsed.at("kind") == "frustratometer" { + return "frustr" + } + if type(metric) == str and metric.contains("\n") { + return "source" + } + if type(metric) == str and parsed.at("kind") == "stackedbars" and metric.contains("(") { + return "inline" + } + if type(metric) == str and parsed.at("kind") != "stackedbars" and metric.contains(",") { + return "inline" + } + "source" +} + +#let _graph-series(parsed, alignment, sequence, selected) = { + let source = _graph-data-source(parsed) + let stacked = _graph-is-stacked(parsed) + if source == "builtin" { + let out = () + for col in selected { + if stacked { + out.push((_builtin-graph-value(alignment, sequence, col, parsed.at("metric")),)) + } else { + out.push(_builtin-graph-value(alignment, sequence, col, parsed.at("metric"))) + } + } + return out + } + let rows = if source == "inline" { + _parse-inline-graph-data(parsed.at("metric"), stacked: stacked) + } else if source == "frustr" { + _read-frustr-data(parsed.at("metric")) + } else { + _read-graph-data(parsed.at("metric"), stacked: stacked) + } + let numbered = rows.any(row => row.at("position") != none) + let out = () + if numbered { + let lookup = (:) + for row in rows { + if row.at("position") != none { + lookup.insert(str(row.at("position")), row.at("values")) + } + } + for col in selected { + let pos = sequence.at("positions").at(col) + let values = if pos == none or not lookup.keys().contains(str(pos)) { () } else { lookup.at(str(pos)) } + if stacked { + out.push(values) + } else { + out.push(if values.len() > 0 { values.first() } else { none }) + } + } + } else { + for idx in range(0, selected.len()) { + if idx < rows.len() { + let values = rows.at(idx).at("values") + out.push(if stacked { values } else { if values.len() > 0 { values.first() } else { none } }) + } else { + out.push(if stacked { () } else { none }) + } + } + } + out +} + +#let _graph-normalized(bounds, value) = { + if value == none { + return none + } + let lo = bounds.at(0) + let hi = bounds.at(1) + if hi == lo { + return 0.0 + } + calc.max(0.0, calc.min(1.0, (value - lo) / (hi - lo))) +} + +#let _graph-stretch(config, kind, position) = { + let table = if kind == "color" { config.at("color-scale-stretch") } else { config.at("bar-graph-stretch") } + table.at(position, default: table.at("default")) +} + +#let _graph-bar-colors(parsed, value) = { + let options = parsed.at("options") + let fg = if options.len() >= 1 and options.at(0) != "" { + options.at(0) + } else if parsed.at("metric") == "charge" and value < 0 { + "BrickRed" + } else if parsed.at("metric") == "charge" and value > 0 { + "RoyalBlue" + } else { + "Gray60" + } + let bg = if options.len() >= 2 and options.at(1) != "" { options.at(1) } else { none } + (fg: fg, bg: bg) +} + +#let _graph-color-scale(parsed) = { + let options = parsed.at("options") + if options.len() >= 1 and options.at(0) != "" { + options.at(0) + } else if parsed.at("metric") == "charge" { + "RedBlue" + } else if parsed.at("metric") == "conservation" { + "ColdHot" + } else if parsed.at("metric") == "entropy" or parsed.at("metric") == "gap-fraction" or parsed.at("metric") == "gaps" { + "WhiteBlack" + } else if parsed.at("metric") == "coverage" or parsed.at("metric") == "identity" or parsed.at("metric") == "identity-to-reference" { + "ColdHot" + } else { + "WhiteBlack" + } +} + +#let _stack-colors(parsed, count) = { + let options = parsed.at("options") + if parsed.at("kind") == "frustratometer" { + if options.len() >= 3 { + return (colors: (resolve-color(options.at(0)), resolve-color(options.at(1)), resolve-color(options.at(2))), background: none) + } + return (colors: (resolve-color("PineGreen"), resolve-color("Gray60"), resolve-color("BrickRed")), background: none) + } + if options.len() == 0 { + let out = () + for idx in range(0, count) { + let level = if count <= 1 { 50 } else { calc.round(idx * 100 / (count - 1)) } + out.push(scale-color("BlueRed", level)) + } + return (colors: out, background: none) + } + if _graph-scales.contains(options.first()) { + let out = () + for idx in range(0, count) { + let level = if count <= 1 { 50 } else { calc.round(idx * 100 / (count - 1)) } + out.push(scale-color(options.first(), level)) + } + let background = if options.len() >= 2 and options.at(1) != "" { options.at(1) } else { none } + return (colors: out, background: background) + } + let out = () + for idx in range(0, count) { + out.push(resolve-color(options.at(calc.min(idx, options.len() - 1)))) + } + (colors: out, background: none) +} + +#let _graph-resolution(parsed, config, position) = if parsed.at("kind") == "color" { + calc.max(1, int(calc.ceil(_graph-stretch(config, "color", position)))) +} else { + calc.max(1, int(calc.ceil(8 * _graph-stretch(config, "bar", position)))) +} + +#let _graph-fill-cell(fill, background: none, frame: none) = ( + char: "", + fg: "Black", + bg: fill, + emph: false, + frame: frame, +) + +#let _graph-color-rows(parsed, config, series, selected, segment, position) = { + let rows = () + let levels = () + let bounds = _graph-range-from-style(parsed, series: series) + for value in series { + levels.push(_graph-normalized(bounds, value)) + } + let repeat = _graph-resolution(parsed, config, position) + for _ in range(0, repeat) { + let cells = () + for col in segment { + let idx = _array-position(selected, col) + if idx == none { + cells.push(_empty-cell()) + } else { + let level = levels.at(idx) + if level == none { + cells.push(_empty-cell()) + } else { + cells.push(_graph-fill-cell(scale-color(_graph-color-scale(parsed), calc.round(level * 100)), frame: if parsed.at("metric") == "conservation" { resolve-color("Gray40") } else { none })) + } + } + } + rows.push(cells) + } + rows +} + +#let _bar-segment(level, lo, hi) = level >= lo and level < hi + +#let _graph-bar-rows(parsed, config, series, selected, segment, position) = { + let rows = () + let bounds = _graph-range-from-style(parsed, series: series) + let baseline = if bounds.at(0) < 0 and bounds.at(1) > 0 { _graph-normalized(bounds, 0.0) } else if bounds.at(1) <= 0 { 1.0 } else { 0.0 } + let repeat = _graph-resolution(parsed, config, position) + let bg = if parsed.at("options").len() >= 2 and parsed.at("options").at(1) != "" { parsed.at("options").at(1) } else { none } + for row in range(0, repeat) { + let level = 1.0 - row / repeat + let cells = () + for col in segment { + let idx = _array-position(selected, col) + if idx == none { + cells.push(_empty-cell()) + } else { + let value = series.at(idx) + let normalized = _graph-normalized(bounds, value) + if normalized == none { + cells.push(_empty-cell()) + } else { + let start = calc.min(normalized, baseline) + let stop = calc.max(normalized, baseline) + if _bar-segment(level, start, stop + 1.0 / repeat) { + let colors = _graph-bar-colors(parsed, value) + cells.push(_graph-fill-cell(resolve-color(colors.at("fg")), background: colors.at("bg"))) + } else if bg != none { + cells.push(_graph-fill-cell(resolve-color(bg))) + } else { + cells.push(_empty-cell()) + } + } + } + } + rows.push(cells) + } + rows +} + +#let _graph-stacked-rows(parsed, config, series, selected, segment, position) = { + let rows = () + let bounds = _graph-range-from-style(parsed, series: series) + let baseline = if bounds.at(0) < 0 and bounds.at(1) > 0 { _graph-normalized(bounds, 0.0) } else if bounds.at(1) <= 0 { 1.0 } else { 0.0 } + let max-count = 0 + for values in series { + if values.len() > max-count { + max-count = values.len() + } + } + let palette = _stack-colors(parsed, max-count) + let repeat = _graph-resolution(parsed, config, position) + for row in range(0, repeat) { + let level = 1.0 - row / repeat + let cells = () + for col in segment { + let idx = _array-position(selected, col) + if idx == none { + cells.push(_empty-cell()) + } else { + let values = series.at(idx) + let segments = () + let pos-cum = 0.0 + let neg-cum = 0.0 + for (value-index, value) in values.enumerate() { + if value == none { + continue + } + if value >= 0 { + let start = _graph-normalized(bounds, pos-cum) + pos-cum += value + let stop = _graph-normalized(bounds, pos-cum) + segments.push((lo: start, hi: stop, color: palette.at("colors").at(calc.min(value-index, palette.at("colors").len() - 1)))) + } else { + let start = _graph-normalized(bounds, neg-cum) + neg-cum += value + let stop = _graph-normalized(bounds, neg-cum) + segments.push((lo: stop, hi: start, color: palette.at("colors").at(calc.min(value-index, palette.at("colors").len() - 1)))) + } + } + let fill = none + for segment-data in segments { + if _bar-segment(level, segment-data.at("lo"), segment-data.at("hi") + 1.0 / repeat) { + fill = segment-data.at("color") + } + } + if fill != none { + cells.push(_graph-fill-cell(fill)) + } else if palette.at("background") != none { + cells.push(_graph-fill-cell(resolve-color(palette.at("background")))) + } else if baseline != none and calc.abs(level - baseline) <= 1.0 / repeat { + cells.push(_graph-fill-cell(resolve-color("Gray30"))) + } else { + cells.push(_empty-cell()) + } + } + } + rows.push(cells) + } + rows +} + +#let _graph-rows(alignment, config, sequence, selected, segment, style, position: none) = { + let parsed = _graph-style(style) + if parsed == none { + return () + } + let series = _graph-series(parsed, alignment, sequence, selected) + if parsed.at("kind") == "color" { + return _graph-color-rows(parsed, config, series, selected, segment, position) + } else if parsed.at("kind") == "stackedbars" or parsed.at("kind") == "frustratometer" { + return _graph-stacked-rows(parsed, config, series, selected, segment, position) + } else { + return _graph-bar-rows(parsed, config, series, selected, segment, position) + } +} diff --git a/packages/preview/typshade/0.1.3/internal/render/logos.typ b/packages/preview/typshade/0.1.3/internal/render/logos.typ new file mode 100644 index 0000000000..f03535504d --- /dev/null +++ b/packages/preview/typshade/0.1.3/internal/render/logos.typ @@ -0,0 +1,160 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "../model/logo.typ": _logo-column-items, _logo-max-bits, _logo-residue-color +#import "../model/palette.typ": resolve-color +#import "../model/parser.typ": _lower +#import "../model/text-style.typ": _text-params, _text-string + +#let _logo-height(config) = 28pt * config.at("sequence-logo").at("stretch") + +#let _logo-residue(item, config, seq-type, subfamily) = { + let value = item.at("value") + let size = 5pt + 18pt * (calc.abs(value) / _logo-max-bits(seq-type)) + let residue = if subfamily and value < 0 { _lower(item.at("residue")) } else { item.at("residue") } + text(.._text-params(config, "residues", fill: resolve-color(_logo-residue-color(config, seq-type, item.at("residue"), subfamily: subfamily)), size: size))[#_text-string(config, "residues", residue)] +} + +#let _logo-stack(items, config, seq-type, subfamily, height, edge) = { + box(height: height, inset: 0pt)[#align(edge + center, stack(spacing: -1pt, ..items.map(item => _logo-residue(item, config, seq-type, subfamily))))] +} + +#let _logo-scale-content(config, seq-type, subfamily: false) = { + let max-bits = calc.round(_logo-max-bits(seq-type) * 10) / 10 + let logo-scale-color = resolve-color(config.at("logo-scale").at("color")) + let height = _logo-height(config) + let negative = subfamily and config.at("subfamily-logo").at("show-negatives") + if not negative { + return box(height: height, inset: 0pt)[#align(center + horizon, stack( + spacing: 1pt, + text(.._text-params(config, "ruler", fill: logo-scale-color, size: 6pt))[#str(max-bits)], + rect(width: 0.6pt, height: height - 14pt, fill: logo-scale-color), + text(.._text-params(config, "ruler", fill: logo-scale-color, size: 6pt))[0], + ))] + } + box(height: height + 12pt, inset: 0pt)[#align(center + horizon, stack( + spacing: 1pt, + text(.._text-params(config, "ruler", fill: logo-scale-color, size: 6pt))[#str(max-bits)], + rect(width: 0.6pt, height: height / 2 - 5pt, fill: logo-scale-color), + text(.._text-params(config, "ruler", fill: logo-scale-color, size: 6pt))[0], + rect(width: 0.6pt, height: height / 2 - 5pt, fill: logo-scale-color), + text(.._text-params(config, "ruler", fill: logo-scale-color, size: 6pt))[-#str(max-bits)], + ))] +} + +#let _logo-name-cell(config, subfamily: false) = { + let height = _logo-height(config) + let label-color = resolve-color(config.at("legend").at("color")) + if subfamily and config.at("subfamily-logo").at("show-negatives") { + return box(height: height + 12pt, inset: 0pt)[#align(left + horizon, stack( + spacing: 1pt, + box(height: 6pt, inset: 0pt)[], + box(height: height / 2, inset: 0pt)[#align(left + bottom, text(.._text-params(config, "names", fill: label-color))[#_text-string(config, "names", config.at("subfamily-logo").at("name"))])], + box(height: 0pt, inset: 0pt)[], + box(height: height / 2, inset: 0pt)[#align(left + top, text(.._text-params(config, "names", fill: label-color))[#_text-string(config, "names", config.at("subfamily-logo").at("negative-name"))])], + box(height: 6pt, inset: 0pt)[], + ))] + } + let label = if subfamily { config.at("subfamily-logo").at("name") } else { config.at("sequence-logo").at("name") } + box(height: height, inset: 0pt)[#align(left + horizon, text(.._text-params(config, "names", fill: label-color))[#_text-string(config, "names", label)])] +} + +#let _logo-column(alignment, config, col, subfamily: false) = { + let items = _logo-column-items(alignment, config, col, subfamily: subfamily) + if items.len() == 0 { + return box(height: if subfamily and config.at("subfamily-logo").at("show-negatives") { _logo-height(config) + 12pt } else { _logo-height(config) }, inset: 0pt)[] + } + let seq-type = alignment.at("seq-type") + if not subfamily { + return _logo-stack(items, config, seq-type, false, _logo-height(config), bottom) + } + let positive = items.filter(it => it.at("value") > 0) + let negative = if config.at("subfamily-logo").at("show-negatives") { items.filter(it => it.at("value") < 0) } else { () } + let pos-hit = config.at("relevance").at("show") and positive.any(it => it.at("value") >= config.at("relevance").at("threshold")) + let neg-hit = config.at("relevance").at("show") and negative.any(it => -it.at("value") >= config.at("relevance").at("threshold")) + box(height: _logo-height(config) + 12pt, inset: 0pt)[#align(center + horizon, stack( + spacing: 0pt, + box(height: 6pt, inset: 0pt)[#align(center + horizon, if pos-hit { text(.._text-params(config, "ruler", fill: resolve-color(config.at("relevance").at("color")), size: 6pt))[#_text-string(config, "ruler", config.at("relevance").at("char"))]} else { [] })], + _logo-stack(positive, config, seq-type, true, _logo-height(config) / 2, bottom), + rect(width: 80%, height: 0.35pt, fill: resolve-color(config.at("logo-scale").at("color"))), + _logo-stack(negative, config, seq-type, true, _logo-height(config) / 2, top), + box(height: 6pt, inset: 0pt)[#align(center + horizon, if neg-hit { text(.._text-params(config, "ruler", fill: resolve-color(config.at("relevance").at("color")), size: 6pt))[#_text-string(config, "ruler", config.at("relevance").at("char"))]} else { [] })], + ))] +} + +#let _logo-table(alignment, config, segment, name-width, num-width, cell-width, subfamily: false) = { + let columns = () + let items = () + if config.at("names").at("show") and config.at("names").at("position") == "left" { + columns.push(name-width) + items.push(_logo-name-cell(config, subfamily: subfamily)) + } + if config.at("numbering").at("show") and config.at("numbering").at("left") { + columns.push(num-width) + items.push([]) + } + for col in segment { + columns.push(cell-width) + items.push(_logo-column(alignment, config, col, subfamily: subfamily)) + } + if config.at("numbering").at("show") and config.at("numbering").at("right") { + columns.push(num-width) + items.push([]) + } + if config.at("names").at("show") and config.at("names").at("position") == "right" { + columns.push(name-width) + items.push(_logo-name-cell(config, subfamily: subfamily)) + } + table(columns: columns, inset: 0pt, stroke: none, align: center, column-gutter: 0pt, row-gutter: 0pt, ..items) +} + +#let _logo-block(alignment, config, segment, name-width, num-width, cell-width, subfamily: false) = { + let columns = () + let items = () + let show-left = config.at("logo-scale").at("show") and (config.at("logo-scale").at("position") == "left" or config.at("logo-scale").at("position") == "leftright") + let show-right = config.at("logo-scale").at("show") and (config.at("logo-scale").at("position") == "right" or config.at("logo-scale").at("position") == "leftright") + if show-left { + columns.push(18pt) + items.push(_logo-scale-content(config, alignment.at("seq-type"), subfamily: subfamily)) + } + columns.push(auto) + items.push(_logo-table(alignment, config, segment, name-width, num-width, cell-width, subfamily: subfamily)) + if show-right { + columns.push(18pt) + items.push(_logo-scale-content(config, alignment.at("seq-type"), subfamily: subfamily)) + } + table(columns: columns, inset: 0pt, stroke: none, column-gutter: 2pt, row-gutter: 0pt, ..items) +} + +#let _legend-entry-table(config, items) = { + if items.len() == 0 { + return none + } + let cells = () + for item in items { + cells.push(box(width: 10pt, height: 10pt, fill: resolve-color(item.at("bg")), stroke: none)[]) + cells.push(text(.._text-params(config, "legend", fill: resolve-color(config.at("legend").at("color"))))[#_text-string(config, "legend", item.at("label"))]) + } + move(dx: config.at("legend").at("dx"), dy: config.at("legend").at("dy"))[ + #table(columns: (10pt, auto), inset: (x: 2pt, y: 1pt), stroke: none, column-gutter: 4pt, row-gutter: 2pt, ..cells) + ] +} + +#let _legend-block(config, items: ()) = { + if not config.at("legend").at("show") { + return none + } + if config.at("shading").at("mode") == "functional" { + let option = config.at("shading").at("option", default: none) + let mode = if option == none { config.at("functional-default") } else { option } + if not config.at("functional-groups").keys().contains(mode) { + return none + } + let functional-items = () + for group in config.at("functional-groups").at(mode) { + functional-items.push((label: group.at("name"), fg: group.at("fg"), bg: group.at("bg"))) + } + return _legend-entry-table(config, functional-items) + } + _legend-entry-table(config, items) +} diff --git a/packages/preview/typshade/0.1.3/justfile b/packages/preview/typshade/0.1.3/justfile new file mode 100644 index 0000000000..16f44b59bc --- /dev/null +++ b/packages/preview/typshade/0.1.3/justfile @@ -0,0 +1,64 @@ +TOML_FILE := "typst.toml" + +get_value_from_toml key: + #!/bin/bash + grep "{{key}}" {{TOML_FILE}} | sed -E 's/.*= "(.*)"/\1/' + +get_local_package_dir: + #!/bin/bash + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + echo "$HOME/Library/Application Support/typst/packages" + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + echo "${XDG_DATA_HOME:-$HOME/.local/share}/typst/packages" + elif [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "cygwin"* ]]; then + # Windows + echo "$APPDATA/typst/packages" + else + echo "Unsupported OS" + exit 1 + fi + +install: + #!/bin/bash + if ! command -v typst &> /dev/null; then + echo "Error: Typst is not installed. Please install it before running this recipe." + exit 1 + fi + + PACKAGE_NAME=$(just get_value_from_toml name) + PACKAGE_VERSION=$(just get_value_from_toml version) + LOCAL_PACKAGE_DIR=$(just get_local_package_dir) + TARGET_DIR="$LOCAL_PACKAGE_DIR/local/$PACKAGE_NAME/$PACKAGE_VERSION" + + mkdir -p "$TARGET_DIR" + + echo "Copying files to $TARGET_DIR..." + + cp {{TOML_FILE}} "$TARGET_DIR" + + cp *.typ "$TARGET_DIR" + + if [ -d "internal" ]; then + cp -r internal "$TARGET_DIR" + fi + + [ -f "README.md" ] && cp README.md "$TARGET_DIR" + [ -f "LICENSE" ] && cp LICENSE "$TARGET_DIR" + + echo "Package $PACKAGE_NAME version $PACKAGE_VERSION has been installed." + +clean: + #!/bin/bash + PACKAGE_NAME=$(just get_value_from_toml name) + PACKAGE_VERSION=$(just get_value_from_toml version) + LOCAL_PACKAGE_DIR=$(just get_local_package_dir) + TARGET_DIR="$LOCAL_PACKAGE_DIR/local/$PACKAGE_NAME/$PACKAGE_VERSION" + + if [ -d "$TARGET_DIR" ]; then + rm -rf "$TARGET_DIR" + echo "Directory $TARGET_DIR has been removed." + else + echo "Directory $TARGET_DIR does not exist." + fi \ No newline at end of file diff --git a/packages/preview/typshade/0.1.3/lib.typ b/packages/preview/typshade/0.1.3/lib.typ new file mode 100644 index 0000000000..e836668b9e --- /dev/null +++ b/packages/preview/typshade/0.1.3/lib.typ @@ -0,0 +1,15 @@ +// Copyright (C) 2026 Eito Yoneyama +// SPDX-License-Identifier: GPL-2.0 + +#import "internal/interface/annotations.typ": * +#import "internal/interface/analysis.typ": * +#import "internal/interface/controls.typ": * +#import "internal/interface/data.typ": alignment-data, parse-alignment +#import "internal/interface/inspect.typ": * +#import "internal/interface/presets.typ": shade-preset, shade-theme, visual-theme +#import "internal/interface/recipes.typ": * +#import "internal/interface/selection.typ": * +#import "internal/interface/shade.typ": * +#import "internal/interface/shortcuts.typ": * +#import "internal/interface/tracks.typ": * +#import "internal/model/palette.typ": resolve-color, scale-color diff --git a/packages/preview/typshade/0.1.3/typst.toml b/packages/preview/typshade/0.1.3/typst.toml new file mode 100644 index 0000000000..7ff83ea8ab --- /dev/null +++ b/packages/preview/typshade/0.1.3/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "typshade" +version = "0.1.3" +entrypoint = "lib.typ" +authors = ["Eito Yoneyama"] +license = "GPL-2.0-only" +description = "Visualize multiple-sequence alignments for bioinformatics." +compiler = "0.14.0" +repository = "https://github.com/rice8y/typshade" +keywords = ["bioinformatics", "alignment", "sequence", "visualization"] +categories = ["visualization", "components"]