clx-browser

[ACTIVE] a smol browser based off of circumflex
git clone git://git.figbert.com/clx-browser.git
Log | Files | Refs | README | LICENSE

commit b2d085dc138ee4f012e50f52a9850bf81f88c837
Author: FIGBERT <figbert@figbert.com>
Date:   Tue, 26 Jul 2022 00:06:10 -0700

Initial commit

Diffstat:
ACOPYING | 661+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 17+++++++++++++++++
Acli/cli.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acomment/comment.go | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconstants/category/category.go | 10++++++++++
Aconstants/margins/margins.go | 8++++++++
Aconstants/nerdfonts/nerdfonts.go | 9+++++++++
Aconstants/style/style.go | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconstants/unicode/unicode.go | 6++++++
Ago.mod | 39+++++++++++++++++++++++++++++++++++++++
Ago.sum | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aitem/item.go | 17+++++++++++++++++
Amain.go | 29+++++++++++++++++++++++++++++
Amarkdown/markdown.go | 27+++++++++++++++++++++++++++
Amarkdown/parser/parser.go | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkdown/postprocessor/bbc.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkdown/postprocessor/filter/filter.go | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkdown/postprocessor/postprocessor.go | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkdown/postprocessor/rules.go | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkdown/postprocessor/wikipedia.go | 17+++++++++++++++++
Amarkdown/preprocessor/preprocessor.go | 26++++++++++++++++++++++++++
Amarkdown/renderer/renderer.go | 495+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ameta/meta.go | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Areader/reader.go | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascreen/screen.go | 39+++++++++++++++++++++++++++++++++++++++
Asettings/core.go | 28++++++++++++++++++++++++++++
Asyntax/syntax.go | 497+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/http/fetcher.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils/strip-ansi/strip-ansi.go | 14++++++++++++++
29 files changed, 3502 insertions(+), 0 deletions(-)

diff --git a/COPYING b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. diff --git a/README.md b/README.md @@ -0,0 +1,17 @@ +# clx-browser + +`clx-browser` is a smol browser based off of [circumflex]. + +## Installing + +To build `clx-browser` from source: + +```sh +go install git.figbert.com/clx-browser@latest +``` + +## Under the Hood + +`clx-browser` uses: a tech stack yet to be determined. + +[circumflex]: https://github.com/bensadeh/circumflex diff --git a/cli/cli.go b/cli/cli.go @@ -0,0 +1,51 @@ +package cli + +import ( + "os" + "os/exec" + "strings" + + "git.figbert.com/clx-browser/constants/unicode" +) + +func Less(input string) { + command := exec.Command("less", + "--RAW-CONTROL-CHARS", + "--pattern="+unicode.ZeroWidthSpace, + "--ignore-case", + "--tilde", + "--use-color", + "-P?e"+"\u001B[48;5;237m "+"\u001B[38;5;200m"+"e"+"\u001B[38;5;214m"+"n"+"\u001B[38;5;69m"+"d "+"\033[0m", + "-DSy", + "-DP-") + + command.Stdin = strings.NewReader(input) + command.Stdout = os.Stdout + + if err := command.Run(); err != nil { + panic(err) + } +} + +func WrapLess(input string) *exec.Cmd { + command := exec.Command("less", + "--RAW-CONTROL-CHARS", + "--pattern="+unicode.ZeroWidthSpace, + "--ignore-case", + "--tilde", + "--use-color", + "-P?e"+"\u001B[48;5;234m "+"\u001B[38;5;200m"+"E"+"\u001B[38;5;214m"+"n"+"\u001B[38;5;69m"+"d "+"\033[0m", + "-DSy", + "-DP-") + + command.Stdin = strings.NewReader(input) + command.Stdout = os.Stdout + + return command +} + +func ClearScreen() { + c := exec.Command("clear") + c.Stdout = os.Stdout + _ = c.Run() +} diff --git a/comment/comment.go b/comment/comment.go @@ -0,0 +1,170 @@ +package comment + +import ( + "strings" + + "git.figbert.com/clx-browser/settings" + "git.figbert.com/clx-browser/syntax" + + "github.com/logrusorgru/aurora/v3" + + text "github.com/MichaelMure/go-term-text" +) + +const ( + reset = "\033[0m" + dimmed = "\033[2m" + italic = "\033[3m" +) + +type comment struct { + sections []*section +} + +type section struct { + isCodeBlock bool + isQuote bool + content string +} + +func Print(c string, config *settings.Config, commentWidth int, availableScreenWidth int) string { + if c == "[deleted]" { + return aurora.Faint(c).String() + } + + c = strings.Replace(c, "<p>", "", 1) + c = strings.ReplaceAll(c, "\n</code></pre>\n", "<p>") + paragraphs := strings.Split(c, "<p>") + + comment := new(comment) + comment.sections = make([]*section, len(paragraphs)) + + for i, paragraph := range paragraphs { + s := new(section) + s.content = syntax.ReplaceCharacters(paragraph) + + if strings.Contains(s.content, "<pre><code>") { + s.isCodeBlock = true + } + + if isQuote(s.content) { + s.isQuote = true + } + + comment.sections[i] = s + } + + output := "" + + for i, s := range comment.sections { + paragraph := s.content + + switch { + case s.isQuote: + paragraph = strings.ReplaceAll(paragraph, "<i>", "") + paragraph = strings.ReplaceAll(paragraph, "</i>", "") + paragraph = strings.ReplaceAll(paragraph, "</a>", reset+dimmed+italic) + paragraph = syntax.ReplaceSymbols(paragraph) + paragraph = replaceSmileys(paragraph, config.EmojiSmileys) + + paragraph = strings.Replace(paragraph, ">>", "", 1) + paragraph = strings.Replace(paragraph, ">", "", 1) + paragraph = strings.TrimLeft(paragraph, " ") + paragraph = syntax.TrimURLs(paragraph, false) + paragraph = syntax.RemoveUnwantedNewLines(paragraph) + paragraph = syntax.RemoveUnwantedWhitespace(paragraph) + + paragraph = italic + dimmed + paragraph + reset + + quoteIndent := " " + config.IndentationSymbol + padding := text.WrapPad(dimmed + quoteIndent) + wrappedAndPaddedComment, _ := text.Wrap(paragraph, commentWidth, padding) + paragraph = wrappedAndPaddedComment + + case s.isCodeBlock: + paragraph = syntax.ReplaceHTML(paragraph) + wrappedComment, _ := text.Wrap(paragraph, availableScreenWidth) + + codeLines := strings.Split(wrappedComment, "\n") + formattedCodeLines := "" + + for j, codeLine := range codeLines { + isOnLastLine := j == len(codeLines)-1 + + if isOnLastLine { + formattedCodeLines += dimmed + codeLine + reset + + break + } + + formattedCodeLines += dimmed + codeLine + reset + "\n" + } + + paragraph = formattedCodeLines + + default: + paragraph = syntax.ReplaceSymbols(paragraph) + paragraph = replaceSmileys(paragraph, config.EmojiSmileys) + + paragraph = syntax.ReplaceHTML(paragraph) + paragraph = strings.TrimLeft(paragraph, " ") + paragraph = highlightCommentSyntax(paragraph, config.HighlightComments, config.EnableNerdFonts) + + paragraph = syntax.TrimURLs(paragraph, config.HighlightComments) + paragraph = syntax.RemoveUnwantedNewLines(paragraph) + paragraph = syntax.RemoveUnwantedWhitespace(paragraph) + + wrappedAndPaddedComment, _ := text.Wrap(paragraph, commentWidth) + paragraph = wrappedAndPaddedComment + } + + separator := getParagraphSeparator(i, len(comment.sections)) + output += paragraph + separator + } + + return output +} + +func replaceSmileys(paragraph string, emojiSmiley bool) string { + if !emojiSmiley { + return paragraph + } + + paragraph = syntax.ConvertSmileys(paragraph) + + return paragraph +} + +func isQuote(text string) bool { + quoteMark := ">" + + return strings.HasPrefix(text, quoteMark) || + strings.HasPrefix(text, " "+quoteMark) || + strings.HasPrefix(text, "<i>"+quoteMark) || + strings.HasPrefix(text, "<i> "+quoteMark) +} + +func getParagraphSeparator(index int, sliceLength int) string { + isAtLastParagraph := index == sliceLength-1 + + if isAtLastParagraph { + return "" + } + + return "\n\n" +} + +func highlightCommentSyntax(input string, commentHighlighting bool, enableNerdFonts bool) string { + if !commentHighlighting { + return input + } + + input = syntax.HighlightBackticks(input) + input = syntax.HighlightMentions(input) + input = syntax.HighlightVariables(input) + input = syntax.HighlightAbbreviations(input) + input = syntax.HighlightReferences(input) + input = syntax.HighlightYCStartupsInHeadlines(input, syntax.Unselected, enableNerdFonts) + + return input +} diff --git a/constants/category/category.go b/constants/category/category.go @@ -0,0 +1,10 @@ +package category + +const ( + FrontPage = 0 + New = 1 + Ask = 2 + Show = 3 + Favorites = 4 + Buffer = 5 +) diff --git a/constants/margins/margins.go b/constants/margins/margins.go @@ -0,0 +1,8 @@ +package margins + +const ( + MainViewLeftMargin = 7 + MainViewRightMarginPageCounter = 5 + CommentSectionLeftMargin = 2 + ReaderViewLeftMargin = 2 +) diff --git a/constants/nerdfonts/nerdfonts.go b/constants/nerdfonts/nerdfonts.go @@ -0,0 +1,9 @@ +package nerdfonts + +const ( + Time = "" + Author = "" + Score = "ﰵ" + Comment = "" + Tag = "" +) diff --git a/constants/style/style.go b/constants/style/style.go @@ -0,0 +1,93 @@ +package style + +import ( + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" +) + +const ( + magentaDark = "200" + yellowDark = "214" + blueDark = "33" + pinkDark = "219" + + orange = "214" + orangeFaint = "94" + + logoBgDark = "#0f1429" + headerBgDark = "#2d3454" + unselectedItemFgDark = "247" + paginatorBgDark = logoBgDark + selectedPageFgDark = unselectedItemFgDark + unselectedPageFgDark = "239" + + magentaLight = magentaDark + yellowLight = "208" + blueLight = blueDark + pinkLight = pinkDark + + logoBgLight = "252" + headerBgLight = "254" + unselectedItemFgLight = "235" + paginatorBgLight = logoBgLight + selectedPageFgLight = unselectedItemFgLight + unselectedPageFgLight = "247" +) + +func GetMagenta() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: magentaLight, Dark: magentaDark} +} + +func GetYellow() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: yellowLight, Dark: yellowDark} +} + +func GetBlue() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: blueLight, Dark: blueDark} +} + +func GetPink() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: pinkLight, Dark: pinkDark} +} + +func GetOrange() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: orange, Dark: orange} +} + +func GetOrangeFaint() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: orangeFaint, Dark: orangeFaint} +} + +func GetLogoBg() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: logoBgLight, Dark: logoBgDark} +} + +func GetHeaderBg() lipgloss.TerminalColor { + profile := termenv.ColorProfile() + + if profile != termenv.TrueColor { + return lipgloss.AdaptiveColor{Light: headerBgLight, Dark: "237"} + } + + return lipgloss.AdaptiveColor{Light: headerBgLight, Dark: headerBgDark} +} + +func GetStatusBarBg() lipgloss.TerminalColor { + return GetHeaderBg() +} + +func GetPaginatorBg() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: paginatorBgLight, Dark: paginatorBgDark} +} + +func GetUnselectedItemFg() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: unselectedItemFgLight, Dark: unselectedItemFgDark} +} + +func GetSelectedPageFg() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: selectedPageFgLight, Dark: selectedPageFgDark} +} + +func GetUnselectedPageFg() lipgloss.TerminalColor { + return lipgloss.AdaptiveColor{Light: unselectedPageFgLight, Dark: unselectedPageFgDark} +} diff --git a/constants/unicode/unicode.go b/constants/unicode/unicode.go @@ -0,0 +1,6 @@ +package unicode + +const ( + ZeroWidthSpace = "\u200b" + NoBreakSpace = " " +) diff --git a/go.mod b/go.mod @@ -0,0 +1,39 @@ +module git.figbert.com/clx-browser + +go 1.18 + +require ( + github.com/JohannesKaufmann/html-to-markdown v1.3.5 + github.com/MichaelMure/go-term-text v0.3.1 + github.com/PuerkitoBio/goquery v1.8.0 + github.com/charmbracelet/glamour v0.5.0 + github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b + github.com/logrusorgru/aurora/v3 v3.0.0 + github.com/wayneashleyberry/terminal-dimensions v1.1.0 +) + +require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.5.0 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65 // indirect + github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/microcosm-cc/bluemonday v1.0.17 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/yuin/goldmark v1.4.4 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect + golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.6 // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum @@ -0,0 +1,233 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/JohannesKaufmann/html-to-markdown v1.3.5 h1:FrP3D5IqpxkNOk97TvbFduSo0JQKs/ZpgjuxpmAEFRA= +github.com/JohannesKaufmann/html-to-markdown v1.3.5/go.mod h1:JNSClIRYICFDiFhw6RBhBeWGnMSSKVZ6sPQA+TK4tyM= +github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0= +github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= +github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65 h1:zx4B0AiwqKDQq+AgqxWeHwbbLJQeidq20hgfP+aMNWI= +github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65/go.mod h1:NPO1+buE6TYOWhUI98/hXLHHJhunIpXRuvDN4xjkCoE= +github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b h1:yrGomo5CP7IvXwSwKbDeaJkhwa4BxfgOO/s1V7iOQm4= +github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b/go.mod h1:LTRGsNyO3/Y6u3ERbz17OiXy2qO1Y+/8QjXpg2ViyEY= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY= +github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= +github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y= +github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s= +github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/wayneashleyberry/terminal-dimensions v1.1.0 h1:EB7cIzBdsOzAgmhTUtTTQXBByuPheP/Zv1zL2BRPY6g= +github.com/wayneashleyberry/terminal-dimensions v1.1.0/go.mod h1:2lc/0eWCObmhRczn2SdGSQtgBooLUzIotkkEGXqghyg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= +github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210505214959-0714010a04ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/item/item.go b/item/item.go @@ -0,0 +1,17 @@ +package item + +type Item struct { + ID int + Title string + Points int + User string + Time int64 + TimeAgo string + Type string + URL string + Level int + Domain string + Comments []*Item + Content string + CommentsCount int +} diff --git a/main.go b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "git.figbert.com/clx-browser/cli" + "git.figbert.com/clx-browser/markdown/parser" + "git.figbert.com/clx-browser/markdown/postprocessor" + "git.figbert.com/clx-browser/markdown/renderer" + "git.figbert.com/clx-browser/reader" +) + +func main() { + var ( + title = "FIGBERT" + url = "https://figbert.com" + indent = " ▎" + ) + + article, err := reader.GetNew(url) + if err != nil { + panic(err) + } + + blocks := parser.Parse(article) + header := renderer.CreateHeader(title, url, 70) + renderedArticle := renderer.ToString(blocks, 70, indent) + renderedArticle = postprocessor.Process(header+renderedArticle, url) + + cli.Less(renderedArticle) +} diff --git a/markdown/markdown.go b/markdown/markdown.go @@ -0,0 +1,27 @@ +package markdown + +const ( + Text = 0 + Image = 1 + H1 = 2 + H2 = 3 + H3 = 4 + H4 = 5 + H5 = 6 + H6 = 7 + Quote = 8 + Code = 9 + List = 10 + Table = 11 + Divider = 12 + + ItalicStart = "[CLX-ITALIC]" + ItalicStop = "[CLX-ITALIC-STOP]" + BoldStart = "[CLX-BOLD]" + BoldStop = "[CLX-BOLD-STOP]" +) + +type Block struct { + Kind int + Text string +} diff --git a/markdown/parser/parser.go b/markdown/parser/parser.go @@ -0,0 +1,256 @@ +package parser + +import ( + "errors" + "regexp" + "strings" + + "git.figbert.com/clx-browser/markdown" +) + +func Parse(text string) []*markdown.Block { + var blocks []*markdown.Block + + enDash := "–" + emDash := "—" + normalDash := "-" + + // en- and em-dashes are occasionally used or list items. + // converting them to normal dashes lets us parse more list items. + text = strings.ReplaceAll(text, enDash, normalDash) + text = strings.ReplaceAll(text, emDash, normalDash) + + text = strings.ReplaceAll(text, markdown.BoldStart, "") + text = strings.ReplaceAll(text, markdown.BoldStop, "") + + lines := strings.Split(text+"\n", "\n") + temp := new(tempBuffer) + + isInsideQuote := false + isInsideCode := false + isInsideText := false + isInsideList := false + isInsideTable := false + + for _, line := range lines { + lineWithoutFormatting := strings.TrimLeft(line, " ") + lineWithoutFormatting = strings.ReplaceAll(line, markdown.BoldStart, "") + lineWithoutFormatting = strings.ReplaceAll(line, markdown.ItalicStart, "") + + if isInsideCode { + if strings.HasPrefix(lineWithoutFormatting, "```") { + isInsideCode = false + + appendedBlocks, err := appendNonEmptyBuffer(temp, blocks) + if err == nil { + blocks = appendedBlocks + } + + temp.reset() + + continue + } + + temp.append("\n" + line) + + continue + } + + if line == "" { + appendedBlocks, err := appendNonEmptyBuffer(temp, blocks) + if err == nil { + blocks = appendedBlocks + } + + temp.reset() + + isInsideQuote = false + isInsideText = false + isInsideList = false + isInsideTable = false + + continue + } + + if isInsideTable { + temp.append("\n" + line) + + continue + } + + if isInsideText { + temp.append(" " + line) + + continue + } + + if isInsideList { + temp.append("\n" + line) + + continue + } + + if isInsideQuote { + line = strings.TrimPrefix(line, ">") + line = strings.TrimPrefix(line, " ") + + temp.append("\n" + line) + + continue + } + + switch { + case strings.HasPrefix(lineWithoutFormatting, `![`): + temp.kind = markdown.Image + temp.text = line + + case strings.HasPrefix(lineWithoutFormatting, "> "): + temp.kind = markdown.Quote + temp.text = strings.TrimPrefix(line, "> ") + + isInsideQuote = true + + case strings.HasPrefix(lineWithoutFormatting, "```"): + temp.kind = markdown.Code + temp.text = "" + + isInsideCode = true + + case isListItem(lineWithoutFormatting): + if isSameTypeAsPreviousItem(markdown.List, blocks) { + lastItem := len(blocks) - 1 + + temp.kind = markdown.List + temp.text = blocks[lastItem].Text + "\n" + line + + blocks = RemoveIndex(blocks, lastItem) + isInsideList = true + + continue + } + + temp.kind = markdown.List + temp.text = line + + isInsideList = true + + case strings.HasPrefix(lineWithoutFormatting, "|"): + if isSameTypeAsPreviousItem(markdown.Table, blocks) { + lastItem := len(blocks) - 1 + + temp.kind = markdown.Table + temp.text = blocks[lastItem].Text + "\n" + line + + blocks = RemoveIndex(blocks, lastItem) + isInsideTable = true + + continue + } + + temp.kind = markdown.Table + temp.text = line + + isInsideTable = true + + case strings.HasPrefix(lineWithoutFormatting, "* * *"): + temp.kind = markdown.Divider + temp.text = line + + case strings.HasPrefix(lineWithoutFormatting, "# "): + temp.kind = markdown.H1 + temp.text = lineWithoutFormatting + + isInsideText = true + + case strings.HasPrefix(lineWithoutFormatting, "## "): + temp.kind = markdown.H2 + temp.text = lineWithoutFormatting + + isInsideText = true + + case strings.HasPrefix(lineWithoutFormatting, "### "): + temp.kind = markdown.H3 + temp.text = lineWithoutFormatting + + isInsideText = true + + case strings.HasPrefix(lineWithoutFormatting, "#### "): + temp.kind = markdown.H4 + temp.text = lineWithoutFormatting + + isInsideText = true + + case strings.HasPrefix(lineWithoutFormatting, "##### "): + temp.kind = markdown.H5 + temp.text = lineWithoutFormatting + + isInsideText = true + + case strings.HasPrefix(lineWithoutFormatting, "###### "): + temp.kind = markdown.H6 + temp.text = lineWithoutFormatting + + isInsideText = true + + default: + temp.kind = markdown.Text + temp.text = line + + isInsideText = true + } + } + + return blocks +} + +func RemoveIndex(s []*markdown.Block, index int) []*markdown.Block { + return append(s[:index], s[index+1:]...) +} + +func isListItem(text string) bool { + if text == "" { + return false + } + + exp := regexp.MustCompile(`^\s*(-|\d+\. )`) + listToken := exp.FindString(text) + + return listToken != "" +} + +func isSameTypeAsPreviousItem(itemType int, blocks []*markdown.Block) bool { + if len(blocks) == 0 { + return false + } + + previousItem := len(blocks) - 1 + + return blocks[previousItem].Kind == itemType +} + +func appendNonEmptyBuffer(temp *tempBuffer, blocks []*markdown.Block) ([]*markdown.Block, error) { + if temp.kind == markdown.Text && temp.text == "" { + return nil, errors.New("buffer is empty") + } + + b := markdown.Block{ + Kind: temp.kind, + Text: temp.text, + } + + return append(blocks, &b), nil +} + +type tempBuffer struct { + kind int + text string +} + +func (b *tempBuffer) reset() { + b.kind = 0 + b.text = "" +} + +func (b *tempBuffer) append(text string) { + b.text += text +} diff --git a/markdown/postprocessor/bbc.go b/markdown/postprocessor/bbc.go @@ -0,0 +1,52 @@ +package postprocessor + +import ( + "strings" + + "git.figbert.com/clx-browser/markdown/postprocessor/filter" + + . "github.com/logrusorgru/aurora/v3" +) + +func processBBC(text string) string { + lines := strings.Split(text, "\n") + output := "" + + for i, line := range lines { + isOnFirstOrLastLine := i == 0 || i == len(lines)-1 + lineNoLeadingWhitespace := strings.TrimLeft(line, " ") + + if len(lineNoLeadingWhitespace) == 1 { + continue + } + + if strings.Contains(line, "(Image credit: ") { + continue + } + + if isOnFirstOrLastLine { + output += line + "\n" + + continue + } + + if filter.IsOnLineBeforeTargetEquals([]string{"--"}, lines, i) || + filter.IsOnLineBeforeTargetEquals([]string{"You may also be interested in:"}, lines, i) { + output += "\n" + + break + } + + image := Cyan("Image: ").Faint().String() + line = strings.ReplaceAll(line, "image source", image) + + caption := Yellow("Caption: ").Faint().String() + line = strings.ReplaceAll(line, "image caption", caption) + + output += line + "\n" + } + + output = strings.ReplaceAll(output, "\n\n\n", "\n\n") + + return output +} diff --git a/markdown/postprocessor/filter/filter.go b/markdown/postprocessor/filter/filter.go @@ -0,0 +1,173 @@ +package filter + +import ( + "strings" + + "git.figbert.com/clx-browser/constants/unicode" + ansi "git.figbert.com/clx-browser/utils/strip-ansi" +) + +type RuleSet struct { + skipLineContains []string + skipLineEquals []string + skipParContains []string + skipParEquals []string + endLineContains []string + endLineEquals []string +} + +func (rs *RuleSet) Filter(text string) string { + paragraphs := strings.Split(text, "\n\n") + output := "" + + output = filterByParagraph(paragraphs, output, rs) + + lines := strings.Split(output, "\n") + output = "" + + output = filterByLine(lines, output, rs) + + output = strings.ReplaceAll(output, "\n\n\n\n", "\n\n\n") + output = strings.ReplaceAll(output, "\n\n\n", "\n\n") + output = strings.ReplaceAll(output, "\n\n\n", "\n\n") + output = strings.ReplaceAll(output, "\n\n\n", "\n\n") + + return output +} + +func filterByLine(lines []string, output string, rs *RuleSet) string { + for i, line := range lines { + isOnFirstOrLastLine := i == 0 || i == len(lines)-1 + lineNoLeadingWhitespace := strings.TrimLeft(line, " ") + + if len(lineNoLeadingWhitespace) == 1 { + continue + } + + if equals(rs.skipLineEquals, line) || + contains(rs.skipLineContains, line) { + continue + } + + if isOnFirstOrLastLine { + output += line + "\n" + + continue + } + + if IsOnLineBeforeTargetEquals(rs.endLineEquals, lines, i) || + IsOnLineBeforeTargetContains(rs.endLineContains, lines, i) { + output += "\n" + + break + } + + output += line + "\n" + } + + return output +} + +func filterByParagraph(paragraphs []string, output string, rs *RuleSet) string { + for i, paragraph := range paragraphs { + isOnFirstOrLastParagraph := i == 0 || i == len(paragraphs)-1 + parNoLeadingWhitespace := strings.TrimLeft(paragraph, " ") + + if len(parNoLeadingWhitespace) == 1 { + continue + } + + if equals(rs.skipParEquals, paragraph) || + contains(rs.skipParContains, paragraph) { + continue + } + + if isOnFirstOrLastParagraph { + output += paragraph + "\n\n" + + continue + } + + output += paragraph + "\n\n" + } + + return output +} + +func (rs *RuleSet) SkipLineContains(text string) { + rs.skipLineContains = append(rs.skipLineContains, text) +} + +func (rs *RuleSet) SkipLineEquals(text string) { + rs.skipLineEquals = append(rs.skipLineEquals, text) +} + +func (rs *RuleSet) SkipParContains(text string) { + rs.skipParContains = append(rs.skipParContains, text) +} + +func (rs *RuleSet) SkipParEquals(text string) { + rs.skipParEquals = append(rs.skipParEquals, text) +} + +func (rs *RuleSet) EndBeforeLineContains(text string) { + rs.endLineContains = append(rs.endLineContains, text) +} + +func (rs *RuleSet) EndBeforeLineEquals(text string) { + rs.endLineEquals = append(rs.endLineEquals, text) +} + +func equals(targets []string, line string) bool { + for _, target := range targets { + line = ansi.Strip(line) + line = strings.TrimSpace(line) + line = strings.TrimLeft(line, unicode.ZeroWidthSpace) + + if line == target { + return true + } + } + + return false +} + +func contains(targets []string, line string) bool { + for _, target := range targets { + target = ansi.Strip(target) + if strings.Contains(line, target) { + return true + } + } + + return false +} + +func IsOnLineBeforeTargetEquals(targets []string, lines []string, i int) bool { + for _, target := range targets { + nextLine := lines[i+1] + nextLine = ansi.Strip(nextLine) + nextLine = strings.TrimSpace(nextLine) + nextLine = strings.TrimLeft(nextLine, unicode.ZeroWidthSpace) + + if nextLine == target { + return true + } + } + + return false +} + +func IsOnLineBeforeTargetContains(targets []string, lines []string, i int) bool { + for _, target := range targets { + nextLine := lines[i+1] + nextLine = ansi.Strip(nextLine) + nextLine = strings.TrimLeft(nextLine, " ") + + if strings.Contains(nextLine, target) { + return true + } + } + + return false +} diff --git a/markdown/postprocessor/postprocessor.go b/markdown/postprocessor/postprocessor.go @@ -0,0 +1,66 @@ +package postprocessor + +import ( + "strings" + + "git.figbert.com/clx-browser/constants/margins" + "git.figbert.com/clx-browser/constants/unicode" + "git.figbert.com/clx-browser/screen" + + t "github.com/MichaelMure/go-term-text" +) + +const ( + newLine = "\n" +) + +func Process(text string, url string) string { + text = filterSite(text, url) + text = moveZeroWidthSpaceUpOneLine(text) + text = indent(text) + text = deIndentInfoSection(text) + + return text +} + +func moveZeroWidthSpaceUpOneLine(text string) string { + return strings.ReplaceAll(text, newLine+unicode.ZeroWidthSpace, + unicode.ZeroWidthSpace+newLine) +} + +func indent(commentSection string) string { + indentBlock := strings.Repeat(" ", margins.ReaderViewLeftMargin) + screenWidth := screen.GetTerminalWidth() + + indentedCommentSection, _ := t.WrapWithPad(commentSection, screenWidth, indentBlock) + + return indentedCommentSection +} + +func deIndentInfoSection(commentSection string) string { + var sb strings.Builder + + lines := strings.Split(commentSection, "\n") + + for i, line := range lines { + isOnLastLine := i == len(lines)-1 + isInfoSection := strings.Contains(line, "╭") || strings.Contains(line, "│") || + strings.Contains(line, "╰") + + if isInfoSection { + deIndentedLine := strings.TrimPrefix(line, " ") + + sb.WriteString(deIndentedLine + "\n") + + continue + } + + if isOnLastLine { + continue + } + + sb.WriteString(line + "\n") + } + + return sb.String() +} diff --git a/markdown/postprocessor/rules.go b/markdown/postprocessor/rules.go @@ -0,0 +1,129 @@ +package postprocessor + +import ( + "strings" + + "git.figbert.com/clx-browser/markdown/postprocessor/filter" +) + +func filterSite(text string, url string) string { + ruleSet := filter.RuleSet{} + + switch { + case strings.Contains(url, "en.wikipedia.org"): + text = strings.ReplaceAll(text, "[edit]", "") + text = removeWikipediaReferences(text) + + ruleSet.EndBeforeLineEquals("References") + ruleSet.EndBeforeLineEquals("Footnotes") + + return ruleSet.Filter(text) + + case strings.Contains(url, "bbc.com") || strings.Contains(url, "bbc.co.uk"): + return processBBC(text) + + case strings.Contains(url, "nytimes.com"): + ruleSet.SkipParContains("Credit…") + ruleSet.SkipParContains("This is a developing story. Check back for updates.") + + ruleSet.SkipLineEquals("Credit") + ruleSet.SkipLineEquals("Image") + + return ruleSet.Filter(text) + + case strings.Contains(url, "economist.com"): + ruleSet.SkipParContains("Listen to this story") + ruleSet.SkipParContains("Your browser does not support the ") + ruleSet.SkipParContains("Listen on the go") + ruleSet.SkipParContains("Get The Economist app and play articles") + ruleSet.SkipParContains("Play in app") + ruleSet.SkipParContains("Enjoy more audio and podcasts on iOS or Android") + + ruleSet.EndBeforeLineContains("This article appeared in the") + ruleSet.EndBeforeLineContains("For more coverage of ") + + return ruleSet.Filter(text) + + case strings.Contains(url, "tomshardware.com"): + ruleSet.SkipParContains("1. Home") + ruleSet.SkipParContains("2. News") + ruleSet.SkipParContains("(Image credit: ") + + return ruleSet.Filter(text) + + case strings.Contains(url, "cnn.com"): + ruleSet.SkipParContains("Credit: ") + + return ruleSet.Filter(text) + + case strings.Contains(url, "arstechnica.com"): + ruleSet.SkipParContains("Enlarge/ ") + ruleSet.SkipParContains("This story originally appeared on ") + + return ruleSet.Filter(text) + + case strings.Contains(url, "macrumors.com"): + ruleSet.EndBeforeLineEquals("Top Stories") + ruleSet.EndBeforeLineEquals("Related Stories") + + return ruleSet.Filter(text) + + case strings.Contains(url, "wired.com") || strings.Contains(url, "wired.co.uk"): + ruleSet.SkipParContains("Read more: ") + ruleSet.SkipParContains("Do you use social media regularly? Take our short survey.") + + ruleSet.EndBeforeLineEquals("More Great WIRED Stories") + + return ruleSet.Filter(text) + + case strings.Contains(url, "theguardian.com"): + ruleSet.SkipParContains("Photograph:") + + return ruleSet.Filter(text) + + case strings.Contains(url, "axios.com"): + ruleSet.SkipParContains("Sign up for our daily briefing") + ruleSet.SkipParContains("Catch up on the day's biggest business stories") + ruleSet.SkipParContains("Stay on top of the latest market trends") + ruleSet.SkipParContains("Sports news worthy of your time") + ruleSet.SkipParContains("Tech news worthy of your time") + ruleSet.SkipParContains("Get the inside stories") + ruleSet.SkipParContains("Axios on your phone") + ruleSet.SkipParContains("Catch up on coronavirus stories and special reports") + ruleSet.SkipParContains("Want a daily digest of the top ") + ruleSet.SkipParContains("Get a daily digest of the most important stories ") + ruleSet.SkipParContains("Download for free.") + ruleSet.SkipParContains("Sign up for free.") + ruleSet.SkipParContains("Make your busy days simpler with Axios AM/PM") + ruleSet.SkipParContains("Subscribe to Axios Closer") + ruleSet.SkipParContains("Get breaking news") + ruleSet.SkipParContains("Sign up for Axios") + ruleSet.SkipParContains("Stay up-to-date on the most important and interesting") + + return ruleSet.Filter(text) + + case strings.Contains(url, "9to5mac.com"): + ruleSet.SkipParContains("We use income earning auto affiliate links.") + ruleSet.SkipParContains("Check out 9to5Mac on YouTube for more Apple news:") + + ruleSet.EndBeforeLineEquals("About the Author") + + return ruleSet.Filter(text) + + case strings.Contains(url, "smithsonianmag.com"): + ruleSet.SkipParContains("smithsonianmag.com") + + ruleSet.EndBeforeLineEquals("Like this article?") + + return ruleSet.Filter(text) + + case strings.Contains(url, "cnet.com"): + ruleSet.SkipParContains("Read more:") + ruleSet.SkipParContains("Stay up-to-date on the latest news") + + return ruleSet.Filter(text) + + default: + return text + } +} diff --git a/markdown/postprocessor/wikipedia.go b/markdown/postprocessor/wikipedia.go @@ -0,0 +1,17 @@ +package postprocessor + +import ( + "strconv" + "strings" +) + +func removeWikipediaReferences(input string) string { + inputWithoutReferences := input + + for i := 1; i < 256; i++ { + number := strconv.Itoa(i) + inputWithoutReferences = strings.ReplaceAll(inputWithoutReferences, "["+number+"]", "") + } + + return inputWithoutReferences +} diff --git a/markdown/preprocessor/preprocessor.go b/markdown/preprocessor/preprocessor.go @@ -0,0 +1,26 @@ +package preprocessor + +import ( + "strings" + + "git.figbert.com/clx-browser/markdown" +) + +func ConvertItalicTags(text string) string { + text = strings.ReplaceAll(text, "<i>", markdown.ItalicStart) + text = strings.ReplaceAll(text, "</i>", markdown.ItalicStop) + text = strings.ReplaceAll(text, "<em>", markdown.ItalicStart) + text = strings.ReplaceAll(text, "</em>", markdown.ItalicStop) + + return text +} + +func ConvertBoldTags(text string) string { + text = strings.ReplaceAll(text, "<b>", markdown.BoldStart) + text = strings.ReplaceAll(text, "</b>", markdown.BoldStop) + + text = strings.ReplaceAll(text, "<strong>", markdown.BoldStart) + text = strings.ReplaceAll(text, "</strong>", markdown.BoldStop) + + return text +} diff --git a/markdown/renderer/renderer.go b/markdown/renderer/renderer.go @@ -0,0 +1,495 @@ +package renderer + +import ( + "regexp" + "strings" + + "git.figbert.com/clx-browser/constants/unicode" + "git.figbert.com/clx-browser/markdown" + "git.figbert.com/clx-browser/meta" + "git.figbert.com/clx-browser/syntax" + + "github.com/charmbracelet/glamour" + + terminal "github.com/wayneashleyberry/terminal-dimensions" + + termtext "github.com/MichaelMure/go-term-text" + . "github.com/logrusorgru/aurora/v3" +) + +const ( + indentLevel1 = " " + indentLevel2 = indentLevel1 + indentLevel1 + indentLevel3 = indentLevel2 + indentLevel1 + + codeStart = "[CLX_CODE_START]" + codeEnd = "[CLX_CODE_END]" +) + +func CreateHeader(title string, domain string, lineWidth int) string { + return meta.GetReaderModeMetaBlock(title, domain, lineWidth) +} + +func ToString(blocks []*markdown.Block, lineWidth int, indentBlock string) string { + output := "" + + for _, block := range blocks { + switch block.Kind { + case markdown.Text: + output += renderText(block.Text, lineWidth) + "\n\n" + + case markdown.Image: + output += renderImage(block.Text, lineWidth) + "\n\n" + + case markdown.Code: + output += renderCode(block.Text) + "\n\n" + + case markdown.Quote: + output += renderQuote(block.Text, lineWidth, indentBlock) + "\n\n" + + case markdown.Table: + output += renderTable(block.Text) + "\n\n" + + case markdown.List: + output += renderList(block.Text, lineWidth) + "\n\n" + + case markdown.Divider: + output += renderDivider(lineWidth) + "\n\n" + + case markdown.H1: + output += h1(block.Text, lineWidth) + "\n\n" + + case markdown.H2: + output += h2(block.Text, lineWidth) + "\n\n" + + case markdown.H3: + output += h3(block.Text, lineWidth) + "\n\n" + + case markdown.H4: + output += h4(block.Text, lineWidth) + "\n\n" + + case markdown.H5: + output += h5(block.Text, lineWidth) + "\n\n" + + case markdown.H6: + output += h6(block.Text, lineWidth) + "\n\n" + + default: + output += renderText(block.Text, lineWidth) + "\n\n" + } + } + + output = strings.TrimLeft(output, "\n") + + return output +} + +func renderDivider(lineWidth int) string { + divider := strings.Repeat("-", lineWidth-len(indentLevel1)*2) + + return Faint(indentLevel1 + divider).String() +} + +func renderText(text string, lineWidth int) string { + text = it(text) + text = bld(text) + text = removeHrefs(text) + text = unescapeCharacters(text) + text = removeImageReference(text) + + text = syntax.RemoveUnwantedNewLines(text) + text = highlightBackticks(text) + text = syntax.HighlightMentions(text) + text = syntax.TrimURLs(text, true) + + text, _ = termtext.Wrap(text, lineWidth) + + return text +} + +func renderList(text string, lineWidth int) string { + // Remove unwanted newlines + exp := regexp.MustCompile(`([\w\W[:cntrl:]])(\n)\s*([a-zA-Z\x60(])`) + text = exp.ReplaceAllString(text, `$1 $3`) + + text = it(text) + text = bld(text) + text = removeImageReference(text) + text = removeHrefs(text) + text = unescapeCharacters(text) + text = highlightBackticks(text) + + output := "" + lines := strings.Split(text, "\n") + + for _, line := range lines { + exp := regexp.MustCompile(`^\s*(-|\d+\.)`) + + listToken := exp.FindString(line) + listText := strings.TrimLeft(line, listToken) + + paddingBuffer := strings.Repeat(" ", len(listToken)) + padding := indentLevel1 + paddingBuffer + " " + + wrappedIndentedItem, _ := termtext.WrapWithPadIndent(listToken+listText, lineWidth, indentLevel1, padding) + wrappedIndentedItem = insertSpaceAfterItemListSeparator(wrappedIndentedItem) + + output += wrappedIndentedItem + "\n" + } + + output = replaceListPrefixes(output) + output = trimLeadingZero(output) + + return strings.TrimRight(output, "\n") +} + +func renderImage(text string, lineWidth int) string { + red := "\u001B[31m" + italic := "\u001B[3m" + faint := "\u001B[2m" + normal := "\u001B[0m" + imageLabel := normal + red + faint + "Image " + normal + faint + italic + + text = regexp.MustCompile(`!\[(.*?)\]\(.*?\)$`). + ReplaceAllString(text, imageLabel+`$1`) + + text = regexp.MustCompile(`!\[(.*?)\]\(.*?\)\s`). + ReplaceAllString(text, imageLabel+`$1`) + + text = regexp.MustCompile(`!\[(.*?)\]\(.*?\)`). + ReplaceAllString(text, imageLabel+`$1`) + + if text == imageLabel { + return indentLevel2 + text + normal + } + + lines := strings.Split(text, imageLabel) + output := "" + + for _, line := range lines { + if len(lines) == 1 || len(lines) == 0 { + output += imageLabel + line + "\n\n" + + break + } + + if line == "" { + continue + } + + output += imageLabel + line + "\n\n" + } + + output = strings.TrimSuffix(output, "\n\n") + output += normal + + output = it(output) + output = bld(output) + output = removeDoubleWhitespace(output) + + padding := termtext.WrapPad(indentLevel1) + output, _ = termtext.Wrap(output, lineWidth, padding) + + return output +} + +func renderCode(text string) string { + screenWidth, _ := terminal.Width() + + text = strings.TrimSuffix(text, "\n") + text = strings.TrimPrefix(text, "\n") + + text = Faint(text).String() + text = removeHrefs(text) + + padding := termtext.WrapPad(indentLevel1) + text, _ = termtext.Wrap(text, int(screenWidth), padding) + + return text +} + +func renderQuote(text string, lineWidth int, indentSymbol string) string { + text = Italic(text).Faint().String() + text = unescapeCharacters(text) + text = removeHrefs(text) + text = removeUnwantedNewLines(text) + + indentBlock := " " + indentSymbol + text = itReversed(text) + text = bldInQuote(text) + + padding := termtext.WrapPad(indentLevel1 + Faint(indentBlock).String()) + text, _ = termtext.Wrap(text, lineWidth, padding) + + return text +} + +func removeUnwantedNewLines(text string) string { + paragraphSeparator := "\n\n" + paragraphs := strings.Split(text, paragraphSeparator) + output := "" + + for _, paragraph := range paragraphs { + paragraph = syntax.RemoveUnwantedNewLines(paragraph) + + output += paragraph + paragraphSeparator + } + + output = strings.TrimSuffix(output, paragraphSeparator) + + return output +} + +func renderTable(text string) string { + screenWidth, _ := terminal.Width() + text = strings.ReplaceAll(text, markdown.ItalicStart, "") + text = strings.ReplaceAll(text, markdown.ItalicStop, "") + + text = strings.ReplaceAll(text, markdown.BoldStart, "") + text = strings.ReplaceAll(text, markdown.BoldStop, "") + + text = unescapeCharacters(text) + text = removeImageReference(text) + + r, _ := glamour.NewTermRenderer(glamour.WithStyles(glamour.NoTTYStyleConfig), + glamour.WithWordWrap(int(screenWidth))) + + out, _ := r.Render(text) + + out = strings.ReplaceAll(out, " --- ", " ") + out = strings.TrimPrefix(out, "\n") + out = strings.TrimLeft(out, " ") + out = strings.TrimPrefix(out, "\n") + out = strings.TrimSuffix(out, "\n\n") + + return out +} + +func removeImageReference(text string) string { + exp := regexp.MustCompile(`!\[(.*?)\]\(.*?\)`) + + return exp.ReplaceAllString(text, `$1`) +} + +func it(text string) string { + italic := "\u001B[3m" + noItalic := "\u001B[23m" + + text = strings.ReplaceAll(text, markdown.ItalicStart, italic) + text = strings.ReplaceAll(text, markdown.ItalicStop, noItalic) + + return text +} + +func itReversed(text string) string { + italic := "\u001B[3m" + noItalic := "\u001B[23m" + + text = strings.ReplaceAll(text, markdown.ItalicStart, noItalic) + text = strings.ReplaceAll(text, markdown.ItalicStop, italic) + + return text +} + +func bld(text string) string { + // bold := "\033[31m" + // noBold := "\033[0m" + + text = strings.ReplaceAll(text, markdown.BoldStart, "") + text = strings.ReplaceAll(text, markdown.BoldStop, "") + + return text +} + +func bldInQuote(text string) string { + // bold := "\033[31m" + // noBold := "\033[0m" + + text = strings.ReplaceAll(text, markdown.BoldStart, "") + text = strings.ReplaceAll(text, markdown.BoldStop, "") + + return text +} + +func h1(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).String() + + text, _ = termtext.Wrap(text, lineWidth) + + return unicode.ZeroWidthSpace + text +} + +func h2(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).String() + + text, _ = termtext.Wrap(text, lineWidth) + + return unicode.ZeroWidthSpace + text +} + +func h3(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).Underline().Blue().String() + + text, _ = termtext.Wrap(text, lineWidth) + + return unicode.ZeroWidthSpace + text +} + +func h4(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).Underline().Yellow().String() + + text, _ = termtext.WrapWithPad(text, lineWidth, indentLevel1) + + return unicode.ZeroWidthSpace + text +} + +func h5(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).Underline().Green().String() + + text, _ = termtext.WrapWithPad(text, lineWidth, indentLevel2) + + return unicode.ZeroWidthSpace + text +} + +func h6(text string, lineWidth int) string { + text = preFormatHeader(text) + text = Bold(text).Underline().Cyan().String() + + text, _ = termtext.WrapWithPad(text, lineWidth, indentLevel3) + + return unicode.ZeroWidthSpace + text +} + +func removeHrefs(text string) string { + exp := regexp.MustCompile(`<a href=.+>(.+)</a>`) + text = exp.ReplaceAllString(text, `$1`) + + return text +} + +func insertSpaceAfterItemListSeparator(text string) string { + exp := regexp.MustCompile(`(^\s*-)(\S)`) + + return exp.ReplaceAllString(text, `$1 $2`) +} + +func preFormatHeader(text string) string { + text = removeImageReference(text) + text = strings.TrimLeft(text, "# ") + text = removeBoldAndItalicTags(text) + text = unescapeCharacters(text) + text = it(text) + + return text +} + +func unescapeCharacters(text string) string { + text = strings.ReplaceAll(text, `\|`, "|") + text = strings.ReplaceAll(text, `\-`, "-") + text = strings.ReplaceAll(text, `\_`, "_") + text = strings.ReplaceAll(text, `\*`, "*") + text = strings.ReplaceAll(text, `\\`, `\`) + text = strings.ReplaceAll(text, `\#`, "#") + text = strings.ReplaceAll(text, `\.`, ".") + text = strings.ReplaceAll(text, `\>`, ">") + text = strings.ReplaceAll(text, `\<`, "<") + text = strings.ReplaceAll(text, "\\`", "`") + text = strings.ReplaceAll(text, "...", "…") + text = strings.ReplaceAll(text, `\(`, "(") + + return text +} + +func removeDoubleWhitespace(text string) string { + text = strings.ReplaceAll(text, "  ", " ") + + return text +} + +func removeBoldAndItalicTags(text string) string { + text = strings.ReplaceAll(text, markdown.BoldStart, "") + text = strings.ReplaceAll(text, markdown.BoldStop, "") + + text = strings.ReplaceAll(text, markdown.ItalicStart, "") + text = strings.ReplaceAll(text, markdown.ItalicStop, "") + + return text +} + +func trimLeadingZero(text string) string { + text = strings.ReplaceAll(text, indentLevel2+"01", indentLevel2+" 1") + text = strings.ReplaceAll(text, indentLevel2+"02", indentLevel2+" 2") + text = strings.ReplaceAll(text, indentLevel2+"03", indentLevel2+" 3") + text = strings.ReplaceAll(text, indentLevel2+"04", indentLevel2+" 4") + text = strings.ReplaceAll(text, indentLevel2+"05", indentLevel2+" 5") + text = strings.ReplaceAll(text, indentLevel2+"06", indentLevel2+" 6") + text = strings.ReplaceAll(text, indentLevel2+"07", indentLevel2+" 7") + text = strings.ReplaceAll(text, indentLevel2+"08", indentLevel2+" 8") + text = strings.ReplaceAll(text, indentLevel2+"09", indentLevel2+" 9") + + return text +} + +func highlightBackticks(text string) string { + magenta := "\u001B[35m" + italic := "\u001B[3m" + normal := "\u001B[0m" + + backtick := "`" + numberOfBackticks := strings.Count(text, backtick) + numberOfBackticksIsOdd := numberOfBackticks%2 != 0 + + if numberOfBackticks == 0 || numberOfBackticksIsOdd { + return text + } + + isOnFirstBacktick := true + + for i := 0; i < numberOfBackticks+1; i++ { + if isOnFirstBacktick { + text = strings.Replace(text, backtick, codeStart, 1) + } else { + text = strings.Replace(text, backtick, codeEnd, 1) + } + + isOnFirstBacktick = !isOnFirstBacktick + } + + exp := regexp.MustCompile(`([\S])(\[CLX_CODE_START\])`) + text = exp.ReplaceAllString(text, `$1 $2`) + + text = strings.ReplaceAll(text, "( "+codeStart, "("+codeStart) + + text = strings.ReplaceAll(text, codeStart, normal+magenta+italic) + text = strings.ReplaceAll(text, codeEnd, normal) + + return text +} + +func replaceListPrefixes(text string) string { + lines := strings.Split(text, "\n") + output := "" + + for _, line := range lines { + line = regexp.MustCompile(`^`+strings.Repeat(indentLevel1, 2)+"-"). + ReplaceAllString(line, strings.Repeat(indentLevel1, 2)+"•") + + line = regexp.MustCompile(`^`+strings.Repeat(indentLevel1, 3)+"-"). + ReplaceAllString(line, strings.Repeat(indentLevel1, 3)+"◦") + + line = regexp.MustCompile(`^`+strings.Repeat(indentLevel1, 4)+"-"). + ReplaceAllString(line, strings.Repeat(indentLevel1, 4)+"▪") + + line = regexp.MustCompile(`^`+strings.Repeat(indentLevel1, 5)+"-"). + ReplaceAllString(line, strings.Repeat(indentLevel1, 5)+"▫") + + output += line + "\n" + } + + return output +} diff --git a/meta/meta.go b/meta/meta.go @@ -0,0 +1,172 @@ +package meta + +import ( + "fmt" + "strconv" + + "git.figbert.com/clx-browser/constants/nerdfonts" + + "git.figbert.com/clx-browser/comment" + "git.figbert.com/clx-browser/constants/unicode" + "git.figbert.com/clx-browser/item" + "git.figbert.com/clx-browser/settings" + "git.figbert.com/clx-browser/syntax" + + text "github.com/MichaelMure/go-term-text" + + . "github.com/logrusorgru/aurora/v3" + + "github.com/charmbracelet/lipgloss" +) + +const ( + newLine = "\n" + newParagraph = "\n\n" +) + +func GetReaderModeMetaBlock(title string, url string, lineWidth int) string { + style := lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + PaddingLeft(1). + PaddingRight(1). + Width(lineWidth) + + formattedTitle, _ := text.Wrap(Bold(title).String(), lineWidth) + formattedTitle = unicode.ZeroWidthSpace + newLine + formattedTitle + formattedURL := Blue(text.TruncateMax(url, lineWidth-2)).String() + info := newParagraph + Green("Reader Mode").String() + + return formattedTitle + newParagraph + style.Render(formattedURL+info) + newParagraph +} + +func GetCommentSectionMetaBlock(c *item.Item, config *settings.Config, newComments int) string { + columnWidth := config.CommentWidth/2 - 1 + url := getURL(c.URL, c.Domain, config.CommentWidth) + rootComment := parseRootComment(c.Content, config) + + style := lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + PaddingLeft(1). + PaddingRight(1). + Width(config.CommentWidth) + + leftColumn := lipgloss.NewStyle(). + Width(columnWidth). + Align(lipgloss.Left) + leftColumnText := getAuthor(c.User, config.EnableNerdFonts) + " " + Faint(c.TimeAgo).String() + newLine + + getComments(c.CommentsCount, config.EnableNerdFonts) + getNewCommentsInfo(newComments, config.EnableNerdFonts) + + rightColumn := lipgloss.NewStyle(). + Width(columnWidth). + Align(lipgloss.Right) + rightColumnText := getID(c.ID, config.EnableNerdFonts) + newLine + + getScore(c.Points, config.EnableNerdFonts) + + joined := lipgloss.JoinHorizontal(lipgloss.Left, leftColumn.Render(leftColumnText), + rightColumn.Render(rightColumnText)) + + return getHeadline(c.Title, config) + newParagraph + style.Render(url+joined+rootComment) +} + +func getAuthor(author string, enableNerdFonts bool) string { + if enableNerdFonts { + authorLabel := fmt.Sprintf("%s %s", nerdfonts.Author, author) + + return Red(authorLabel).String() + } + + return fmt.Sprintf("by %s", Red(author).String()) +} + +func getComments(commentsCount int, enableNerdFonts bool) string { + comments := strconv.Itoa(commentsCount) + + if enableNerdFonts { + commentsLabel := fmt.Sprintf("%s %s", nerdfonts.Comment, comments) + + return Magenta(commentsLabel).String() + } + + return fmt.Sprintf("%s comments", Magenta(comments).String()) +} + +func getScore(points int, enableNerdFonts bool) string { + score := strconv.Itoa(points) + + if enableNerdFonts { + pointsLabel := fmt.Sprintf("%s %s", score, nerdfonts.Score) + + return Yellow(pointsLabel).String() + } + + return fmt.Sprintf("%s points", Yellow(score).String()) +} + +func getID(id int, enableNerdFonts bool) string { + if enableNerdFonts { + idLabel := fmt.Sprintf("%d %s", id, nerdfonts.Tag) + + return Green(idLabel).Faint().String() + } + + idLabel := fmt.Sprintf("ID %d", id) + + return Green(idLabel).Faint().String() +} + +func getNewCommentsInfo(newComments int, enableNerdFonts bool) string { + if newComments == 0 { + return "" + } + + comments := strconv.Itoa(newComments) + + if enableNerdFonts { + return fmt.Sprintf(" (%s)", Cyan(comments).String()) + } + + return fmt.Sprintf(" (%s new)", Cyan(comments).String()) +} + +func getHeadline(title string, config *settings.Config) string { + formattedTitle := highlightTitle(unicode.ZeroWidthSpace+" "+newLine+title, config.HighlightHeadlines, + config.EnableNerdFonts) + wrappedHeadline, _ := text.Wrap(formattedTitle, config.CommentWidth) + + return wrappedHeadline +} + +func highlightTitle(title string, highlightHeadlines bool, enableNerdFont bool) string { + highlightedTitle := title + + if highlightHeadlines { + highlightedTitle = syntax.HighlightYCStartupsInHeadlines(highlightedTitle, syntax.HeadlineInCommentSection, enableNerdFont) + highlightedTitle = syntax.HighlightYear(highlightedTitle, syntax.HeadlineInCommentSection, enableNerdFont) + highlightedTitle = syntax.HighlightHackerNewsHeadlines(highlightedTitle, syntax.HeadlineInCommentSection) + highlightedTitle = syntax.HighlightSpecialContent(highlightedTitle, syntax.HeadlineInCommentSection, enableNerdFont) + } + + return Bold(highlightedTitle).String() +} + +func getURL(url string, domain string, lineWidth int) string { + if domain == "" { + return "" + } + + truncatedURL := text.TruncateMax(url, lineWidth-2) + formattedURL := Blue(truncatedURL).String() + newLine + + return formattedURL + newLine +} + +func parseRootComment(c string, config *settings.Config) string { + if c == "" { + return "" + } + + rootComment := comment.Print(c, config, config.CommentWidth-2, config.CommentWidth) + wrappedComment, _ := text.Wrap(rootComment, config.CommentWidth-2) + + return newParagraph + wrappedComment +} diff --git a/reader/reader.go b/reader/reader.go @@ -0,0 +1,111 @@ +package reader + +import ( + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "git.figbert.com/clx-browser/markdown" + "git.figbert.com/clx-browser/markdown/preprocessor" + + "github.com/JohannesKaufmann/html-to-markdown/plugin" + + "github.com/PuerkitoBio/goquery" + + md "github.com/JohannesKaufmann/html-to-markdown" + + "github.com/go-shiori/go-readability" +) + +func GetNew(url string) (string, error) { + art, httpErr := fetch(url) + if httpErr != nil { + return "", fmt.Errorf("could not fetch url: %w", httpErr) + } + + href := md.Rule{ + Filter: []string{"a"}, + Replacement: func(content string, selec *goquery.Selection, opt *md.Options) *string { + // If the span element has not the classname `bb_strike` return nil. + // That way the next rules will apply. In this case the commonmark rules. + // -> return nil -> next rule applies + //if !selec.HasClass("href") { + // return nil + //} + + // Trim spaces so that the following does NOT happen: `~ and cake~`. + // Because of the space it is not recognized as strikethrough. + // -> trim spaces at begin&end of string when inside strong/italic/... + content = strings.TrimSpace(content) + // return md.String("[" + content + "]") + return md.String(content) + }, + } + + italic := md.Rule{ + Filter: []string{"i"}, + Replacement: func(content string, selec *goquery.Selection, opt *md.Options) *string { + // If the span element has not the classname `bb_strike` return nil. + // That way the next rules will apply. In this case the commonmark rules. + // -> return nil -> next rule applies + //if !selec.HasClass("href") { + // return nil + //} + + // Trim spaces so that the following does NOT happen: `~ and cake~`. + // Because of the space it is not recognized as strikethrough. + // -> trim spaces at begin&end of string when inside strong/italic/... + content = strings.TrimSpace(content) + return md.String(markdown.ItalicStart + content + markdown.ItalicStop) + }, + } + + opt := &md.Options{} + converter := md.NewConverter("", true, opt) + converter.AddRules(href) + converter.AddRules(italic) + converter.Use(plugin.Table()) + // converter.AddRules(span) + + art.Content = preprocessor.ConvertItalicTags(art.Content) + art.Content = preprocessor.ConvertBoldTags(art.Content) + + markdown, err := converter.ConvertString(art.Content) + if err != nil { + log.Fatal(err) + } + // fmt.Println("md ->", markdown) + + markdown = strings.ReplaceAll(markdown, "<span>", "") + markdown = strings.ReplaceAll(markdown, "</span>", "") + + return markdown, nil +} + +func fetch(rawURL string) (readability.Article, error) { + client := http.Client{ + Timeout: 5 * time.Second, + } + + response, err := client.Get(rawURL) + if err != nil { + return readability.Article{}, fmt.Errorf("could not fetch rawURL: %w", err) + } + + defer response.Body.Close() + + pageURL, urlErr := url.Parse(rawURL) + if urlErr != nil { + panic(urlErr) + } + + art, readabilityErr := readability.FromReader(response.Body, pageURL) + if readabilityErr != nil { + return readability.Article{}, fmt.Errorf("could not fetch rawURL: %w", readabilityErr) + } + + return art, nil +} diff --git a/screen/screen.go b/screen/screen.go @@ -0,0 +1,39 @@ +package screen + +import ( + terminal "github.com/wayneashleyberry/terminal-dimensions" +) + +func GetTerminalHeight() int { + height, err := terminal.Height() + if err != nil { + panic("Could not determine terminal height") + } + + return int(height) +} + +func GetTerminalWidth() int { + width, err := terminal.Width() + if err != nil { + panic("Could not determine terminal width") + } + + return int(width) +} + +func GetSubmissionsToShow(screenHeight int, maxStories int) int { + topBarHeight := 2 + footerHeight := 2 + adjustedHeight := screenHeight - topBarHeight - footerHeight + + return min(adjustedHeight/2, maxStories) +} + +func min(x, y int) int { + if x > y { + return y + } + + return x +} diff --git a/settings/core.go b/settings/core.go @@ -0,0 +1,28 @@ +package settings + +type Config struct { + CommentWidth int + PlainHeadlines bool + HighlightHeadlines bool + HighlightComments bool + RelativeNumbering bool + EmojiSmileys bool + MarkAsRead bool + HideIndentSymbol bool + IndentationSymbol string + DebugMode bool + EnableNerdFonts bool +} + +func New() *Config { + return &Config{ + CommentWidth: 70, + HighlightHeadlines: true, + HighlightComments: true, + RelativeNumbering: false, + EmojiSmileys: true, + MarkAsRead: true, + HideIndentSymbol: false, + IndentationSymbol: " ▎", + } +} diff --git a/syntax/syntax.go b/syntax/syntax.go @@ -0,0 +1,497 @@ +package syntax + +import ( + "regexp" + "strings" + + "git.figbert.com/clx-browser/constants/style" + "git.figbert.com/clx-browser/constants/unicode" + "github.com/charmbracelet/lipgloss" + "github.com/logrusorgru/aurora/v3" +) + +const ( + newParagraph = "\n\n" + reset = "\033[0m" + bold = "\033[1m" + reverse = "\033[7m" + italic = "\033[3m" + magenta = "\033[35m" + faint = "\033[2m" + green = "\033[32m" + red = "\033[31m" + + Unselected = iota + HeadlineInCommentSection + Selected + MarkAsRead + AddToFavorites + RemoveFromFavorites +) + +func HighlightYCStartupsInHeadlines(comment string, highlightType int, enableNerdFonts bool) string { + var expression *regexp.Regexp + + if enableNerdFonts { + expression = regexp.MustCompile(`\((YC ([SW]\d{2}))\)`) + + highlightedStartup := reset + getYCBarNerdFonts(``+unicode.NoBreakSpace+`$2`, highlightType, enableNerdFonts) + + getHighlight(highlightType) + return expression.ReplaceAllString(comment, highlightedStartup) + } + + expression = regexp.MustCompile(`\((YC [SW]\d{2})\)`) + highlightedStartup := reset + getYCBar(`$1`, highlightType, enableNerdFonts) + + getHighlight(highlightType) + + return expression.ReplaceAllString(comment, highlightedStartup) +} + +func getYCBar(text string, highlightType int, enableNerdFonts bool) string { + switch highlightType { + case Selected: + return label(text, style.GetOrange(), lipgloss.Color("16"), highlightType, enableNerdFonts) + + case MarkAsRead: + return label(text, lipgloss.Color("237"), style.GetOrangeFaint(), highlightType, enableNerdFonts) + + default: + return label(text, lipgloss.Color("232"), style.GetOrange(), highlightType, enableNerdFonts) + } +} + +func getYCBarNerdFonts(text string, highlightType int, enableNerdFonts bool) string { + switch highlightType { + case Selected: + return label(text, style.GetOrange(), lipgloss.Color("16"), highlightType, enableNerdFonts) + + case MarkAsRead: + return label(text, lipgloss.Color("234"), style.GetOrangeFaint(), highlightType, enableNerdFonts) + + default: + return label(text, lipgloss.Color("16"), style.GetOrange(), highlightType, enableNerdFonts) + } +} + +func HighlightYear(comment string, highlightType int, enableNerdFonts bool) string { + expression := regexp.MustCompile(`\((\d{4})\)`) + + content := getYear(`$1`, highlightType, enableNerdFonts) + return expression.ReplaceAllString(comment, reset+content+getHighlight(highlightType)) +} + +func getYear(text string, highlightType int, enableNerdFont bool) string { + switch highlightType { + case Selected: + return label(text, lipgloss.AdaptiveColor{Light: "16", Dark: "16"}, lipgloss.AdaptiveColor{Light: "27", Dark: "214"}, highlightType, enableNerdFont) + + case MarkAsRead: + return label(text, lipgloss.AdaptiveColor{Light: "39", Dark: "94"}, style.GetHeaderBg(), highlightType, enableNerdFont) + + default: + return label(text, lipgloss.AdaptiveColor{Light: "27", Dark: "214"}, style.GetLogoBg(), highlightType, enableNerdFont) + } +} + +func label(text string, fg lipgloss.TerminalColor, bg lipgloss.TerminalColor, highlightType int, enableNerdFonts bool) string { + content := lipgloss.NewStyle(). + Foreground(fg). + Background(bg) + + border := lipgloss.NewStyle(). + Foreground(bg) + + if highlightType == Selected { + border. + Foreground(lipgloss.NoColor{}). + Background(bg). + Reverse(true) + } + + if highlightType == MarkAsRead { + content.Italic(true) + } + + if highlightType == HeadlineInCommentSection { + content.Bold(true) + } + + return reset + + getLeftBorder(bg, highlightType, enableNerdFonts) + + content.Render(text) + + getRightBorder(bg, highlightType, enableNerdFonts) +} + +func getLeftBorder(bg lipgloss.TerminalColor, highlightType int, enableNerdFonts bool) string { + if enableNerdFonts { + borderStyle := getBorderStyle(bg, highlightType, enableNerdFonts) + return borderStyle.Render("") + } + + borderStyle := getBorderStyle(bg, highlightType, enableNerdFonts) + return borderStyle.Render(" ") +} + +func getRightBorder(bg lipgloss.TerminalColor, highlightType int, enableNerdFonts bool) string { + if enableNerdFonts { + borderStyle := getBorderStyle(bg, highlightType, enableNerdFonts) + return borderStyle.Render("") + } + + borderStyle := getBorderStyle(bg, highlightType, enableNerdFonts) + return borderStyle.Render(" ") +} + +func getBorderStyle(bg lipgloss.TerminalColor, highlightType int, enableNerdFonts bool) lipgloss.Style { + if !enableNerdFonts { + return lipgloss.NewStyle(). + Background(bg) + } + + if highlightType == Selected { + return lipgloss.NewStyle(). + Foreground(lipgloss.NoColor{}). + Background(bg). + Reverse(true) + } + + return lipgloss.NewStyle(). + Foreground(bg) +} + +func HighlightHackerNewsHeadlines(title string, highlightType int) string { + askHN := "Ask HN:" + showHN := "Show HN:" + tellHN := "Tell HN:" + thankHN := "Thank HN:" + launchHN := "Launch HN:" + + highlight := getHighlight(highlightType) + + title = strings.ReplaceAll(title, askHN, aurora.Blue(askHN).String()+highlight) + title = strings.ReplaceAll(title, showHN, aurora.Red(showHN).String()+highlight) + title = strings.ReplaceAll(title, tellHN, aurora.Magenta(tellHN).String()+highlight) + title = strings.ReplaceAll(title, thankHN, aurora.Cyan(thankHN).String()+highlight) + title = strings.ReplaceAll(title, launchHN, aurora.Green(launchHN).String()+highlight) + + return title +} + +func getHighlight(highlightType int) string { + switch highlightType { + case HeadlineInCommentSection: + return bold + case Selected: + return reverse + case MarkAsRead: + return faint + italic + case AddToFavorites: + return green + reverse + case RemoveFromFavorites: + return red + reverse + default: + return "" + } +} + +func HighlightSpecialContent(title string, highlightType int, enableNerdFonts bool) string { + highlight := getHighlight(highlightType) + + if enableNerdFonts { + title = strings.ReplaceAll(title, "[audio]", getSpecialContentRoundedBar("", highlightType, enableNerdFonts)+highlight) + title = strings.ReplaceAll(title, "[video]", getSpecialContentRoundedBar("", highlightType, enableNerdFonts)+highlight) + title = strings.ReplaceAll(title, "[pdf]", getSpecialContentRoundedBar("", highlightType, enableNerdFonts)+highlight) + title = strings.ReplaceAll(title, "[PDF]", getSpecialContentRoundedBar("", highlightType, enableNerdFonts)+highlight) + + return title + } + + title = strings.ReplaceAll(title, "[audio]", aurora.Cyan("audio").String()+highlight) + title = strings.ReplaceAll(title, "[video]", aurora.Cyan("video").String()+highlight) + title = strings.ReplaceAll(title, "[pdf]", aurora.Cyan("pdf").String()+highlight) + title = strings.ReplaceAll(title, "[PDF]", aurora.Cyan("PDF").String()+highlight) + + return title +} + +func getSpecialContentRoundedBar(text string, highlightType int, enableNerdFonts bool) string { + switch highlightType { + case Selected: + return label(text, lipgloss.Color("4"), lipgloss.AdaptiveColor{Light: "255", Dark: "16"}, highlightType, enableNerdFonts) + + case MarkAsRead: + return label(text, style.GetUnselectedItemFg(), style.GetHeaderBg(), highlightType, enableNerdFonts) + + default: + return label(text, lipgloss.AdaptiveColor{Light: "255", Dark: "16"}, lipgloss.Color("4"), highlightType, enableNerdFonts) + } +} + +func ConvertSmileys(text string) string { + text = replaceBetweenWhitespace(text, `:)`, "😊") + text = replaceBetweenWhitespace(text, `(:`, "😊") + text = replaceBetweenWhitespace(text, `:-)`, "😊") + text = replaceBetweenWhitespace(text, `:D`, "😄") + text = replaceBetweenWhitespace(text, `=)`, "😃") + text = replaceBetweenWhitespace(text, `=D`, "😃") + text = replaceBetweenWhitespace(text, `;)`, "😉") + text = replaceBetweenWhitespace(text, `;-)`, "😉") + text = replaceBetweenWhitespace(text, `:P`, "😜") + text = replaceBetweenWhitespace(text, `;P`, "😜") + text = replaceBetweenWhitespace(text, `:o`, "😮") + text = replaceBetweenWhitespace(text, `:O`, "😮") + text = replaceBetweenWhitespace(text, `:(`, "😔") + text = replaceBetweenWhitespace(text, `:-(`, "😔") + text = replaceBetweenWhitespace(text, `:/`, "😕") + text = replaceBetweenWhitespace(text, `:-/`, "😕") + text = replaceBetweenWhitespace(text, `-_-`, "😑") + text = replaceBetweenWhitespace(text, `:|`, "😐") + + return text +} + +func replaceBetweenWhitespace(text string, target string, replacement string) string { + if text == target { + return replacement + } + + return strings.ReplaceAll(text, " "+target, " "+replacement) +} + +func RemoveUnwantedNewLines(text string) string { + exp := regexp.MustCompile(`([\w\W[:cntrl:]])(\n)([a-zA-Z0-9" \-<[:cntrl:]…])`) + + return exp.ReplaceAllString(text, `$1`+" "+`$3`) +} + +func RemoveUnwantedWhitespace(text string) string { + singleSpace := " " + doubleSpace := " " + tripleSpace := " " + + text = strings.ReplaceAll(text, tripleSpace, singleSpace) + text = strings.ReplaceAll(text, doubleSpace, singleSpace) + + return text +} + +func HighlightDomain(domain string) string { + if domain == "" { + return reset + } + + return reset + aurora.Faint("("+domain+")").String() +} + +func HighlightReferences(input string) string { + input = strings.ReplaceAll(input, "[0]", "["+aurora.White("0").String()+"]") + input = strings.ReplaceAll(input, "[1]", "["+aurora.Red("1").String()+"]") + input = strings.ReplaceAll(input, "[2]", "["+aurora.Yellow("2").String()+"]") + input = strings.ReplaceAll(input, "[3]", "["+aurora.Green("3").String()+"]") + input = strings.ReplaceAll(input, "[4]", "["+aurora.Blue("4").String()+"]") + input = strings.ReplaceAll(input, "[5]", "["+aurora.Cyan("5").String()+"]") + input = strings.ReplaceAll(input, "[6]", "["+aurora.Magenta("6").String()+"]") + input = strings.ReplaceAll(input, "[7]", "["+aurora.BrightWhite("7").String()+"]") + input = strings.ReplaceAll(input, "[8]", "["+aurora.BrightRed("8").String()+"]") + input = strings.ReplaceAll(input, "[9]", "["+aurora.BrightYellow("9").String()+"]") + input = strings.ReplaceAll(input, "[10]", "["+aurora.BrightGreen("10").String()+"]") + + return input +} + +func ColorizeIndentSymbol(indentSymbol string, level int) string { + switch level { + case 0: + indentSymbol = "" + case 1: + indentSymbol = aurora.Red(indentSymbol).String() + case 2: + indentSymbol = aurora.Yellow(indentSymbol).String() + case 3: + indentSymbol = aurora.Green(indentSymbol).String() + case 4: + indentSymbol = aurora.Cyan(indentSymbol).String() + case 5: + indentSymbol = aurora.Blue(indentSymbol).String() + case 6: + indentSymbol = aurora.Magenta(indentSymbol).String() + case 7: + indentSymbol = aurora.BrightRed(indentSymbol).String() + case 8: + indentSymbol = aurora.BrightYellow(indentSymbol).String() + case 9: + indentSymbol = aurora.BrightGreen(indentSymbol).String() + case 10: + indentSymbol = aurora.BrightCyan(indentSymbol).String() + case 11: + indentSymbol = aurora.BrightBlue(indentSymbol).String() + case 12: + indentSymbol = aurora.BrightMagenta(indentSymbol).String() + case 13: + indentSymbol = aurora.Red(indentSymbol).String() + case 14: + indentSymbol = aurora.Yellow(indentSymbol).String() + case 15: + indentSymbol = aurora.Green(indentSymbol).String() + case 16: + indentSymbol = aurora.Cyan(indentSymbol).String() + case 17: + indentSymbol = aurora.Blue(indentSymbol).String() + case 18: + indentSymbol = aurora.Magenta(indentSymbol).String() + } + + return reset + indentSymbol +} + +func TrimURLs(comment string, highlightComment bool) string { + expression := regexp.MustCompile(`<a href=".*?" rel="nofollow">`) + + if !highlightComment { + return expression.ReplaceAllString(comment, "") + } + + comment = expression.ReplaceAllString(comment, "") + + e := regexp.MustCompile(`https?://([^,"\) \n]+)`) + comment = e.ReplaceAllString(comment, aurora.Blue(`$1`).String()) + + comment = strings.ReplaceAll(comment, "."+reset+" ", reset+". ") + + return comment +} + +func HighlightBackticks(input string) string { + backtick := "`" + numberOfBackticks := strings.Count(input, backtick) + numberOfBackticksIsOdd := numberOfBackticks%2 != 0 + + if numberOfBackticks == 0 || numberOfBackticksIsOdd { + return input + } + + isOnFirstBacktick := true + + for i := 0; i < numberOfBackticks+1; i++ { + if isOnFirstBacktick { + input = strings.Replace(input, backtick, italic+magenta, 1) + } else { + input = strings.Replace(input, backtick, reset, 1) + } + + isOnFirstBacktick = !isOnFirstBacktick + } + + return input +} + +func HighlightMentions(input string) string { + exp := regexp.MustCompile(`((?:^| )\B@[\w.]+)`) + input = exp.ReplaceAllString(input, aurora.Yellow(`$1`).String()) + + input = strings.ReplaceAll(input, aurora.Yellow("@dang").String(), + aurora.Green("@dang").String()) + input = strings.ReplaceAll(input, aurora.Yellow(" @dang").String(), + aurora.Green(" @dang").String()) + + return input +} + +func HighlightVariables(input string) string { + // Highlighting variables inside commands marked with backticks + // messes with the formatting. If there are both backticks and variables + // in the comment, we give priority to the backticks. + numberOfBackticks := strings.Count(input, "`") + if numberOfBackticks > 0 { + return input + } + + exp := regexp.MustCompile(`(\$+[a-zA-Z_\-]+)`) + + return exp.ReplaceAllString(input, aurora.Cyan(`$1`).String()) +} + +func HighlightAbbreviations(input string) string { + iAmNotALawyer := "IANAL" + iAmALawyer := "IAAL" + + input = strings.ReplaceAll(input, iAmNotALawyer, aurora.Red(iAmNotALawyer).String()) + input = strings.ReplaceAll(input, iAmALawyer, aurora.Green(iAmALawyer).String()) + + return input +} + +func ReplaceCharacters(input string) string { + input = strings.ReplaceAll(input, "&#x27;", "'") + input = strings.ReplaceAll(input, "&gt;", ">") + input = strings.ReplaceAll(input, "&lt;", "<") + input = strings.ReplaceAll(input, "&#x2F;", "/") + input = strings.ReplaceAll(input, "&quot;", `"`) + input = strings.ReplaceAll(input, "&#34;", `"`) + input = strings.ReplaceAll(input, "&amp;", "&") + + return input +} + +func ReplaceHTML(input string) string { + input = strings.Replace(input, "<p>", "", 1) + + input = strings.ReplaceAll(input, "<p>", newParagraph) + input = strings.ReplaceAll(input, "<i>", italic) + input = strings.ReplaceAll(input, "</i>", reset) + input = strings.ReplaceAll(input, "</a>", "") + input = strings.ReplaceAll(input, "<pre><code>", "") + input = strings.ReplaceAll(input, "</code></pre>", "") + + return input +} + +func ReplaceSymbols(paragraph string) string { + paragraph = strings.ReplaceAll(paragraph, "...", "…") + paragraph = strings.ReplaceAll(paragraph, "CO2", "CO₂") + + paragraph = replaceDoubleDashesWithEmDash(paragraph) + paragraph = convertFractions(paragraph) + + return paragraph +} + +func replaceDoubleDashesWithEmDash(paragraph string) string { + paragraph = strings.ReplaceAll(paragraph, " -- ", " — ") + + exp := regexp.MustCompile(`([a-zA-Z])--([a-zA-Z])`) + + return exp.ReplaceAllString(paragraph, `$1`+"—"+`$2`) +} + +func convertFractions(text string) string { + text = strings.ReplaceAll(text, " 1/2", " ½") + text = strings.ReplaceAll(text, " 1/3", " ⅓") + text = strings.ReplaceAll(text, " 2/3", " ⅔") + text = strings.ReplaceAll(text, " 1/4", " ¼") + text = strings.ReplaceAll(text, " 3/4", " ¾") + text = strings.ReplaceAll(text, " 1/5", " ⅕") + text = strings.ReplaceAll(text, " 2/5", " ⅖") + text = strings.ReplaceAll(text, " 3/5", " ⅗") + text = strings.ReplaceAll(text, " 4/5", " ⅘") + text = strings.ReplaceAll(text, " 1/6", " ⅙") + text = strings.ReplaceAll(text, " 1/10", " ⅒ ") + + text = strings.ReplaceAll(text, "1/2 ", "½ ") + text = strings.ReplaceAll(text, "1/3 ", "⅓ ") + text = strings.ReplaceAll(text, "2/3 ", "⅔ ") + text = strings.ReplaceAll(text, "1/4 ", "¼ ") + text = strings.ReplaceAll(text, "3/4 ", "¾ ") + text = strings.ReplaceAll(text, "1/5 ", "⅕ ") + text = strings.ReplaceAll(text, "2/5 ", "⅖ ") + text = strings.ReplaceAll(text, "3/5 ", "⅗ ") + text = strings.ReplaceAll(text, "4/5 ", "⅘ ") + text = strings.ReplaceAll(text, "1/6 ", "⅙ ") + text = strings.ReplaceAll(text, "1/10 ", "⅒ ") + + text = strings.ReplaceAll(text, "1/5th", "⅕th") + text = strings.ReplaceAll(text, "1/6th", "⅙th") + text = strings.ReplaceAll(text, "1/10th", "⅒ th") + + return text +} diff --git a/utils/http/fetcher.go b/utils/http/fetcher.go @@ -0,0 +1,57 @@ +package http + +import ( + "fmt" + "strconv" + "time" + + "clx/app" + "clx/constants/category" + "clx/endpoints" + + "github.com/go-resty/resty/v2" +) + +const ( + baseURL = "http://api.hackerwebapp.com/" + page = "?page=" +) + +func FetchStories(page int, category int) ([]*endpoints.Story, error) { + url := getURL(category) + p := strconv.Itoa(page) + + var s []*endpoints.Story + + client := resty.New() + client.SetTimeout(5 * time.Second) + + _, err := client.R(). + SetHeader("User-Agent", app.Name+"/"+app.Version). + SetResult(&s). + Get(url + p) + if err != nil { + return nil, fmt.Errorf("could not fetch stories: %w", err) + } + + return s, nil +} + +func getURL(cat int) string { + switch cat { + case category.FrontPage: + return baseURL + "news" + page + + case category.New: + return baseURL + "newest" + page + + case category.Ask: + return baseURL + "ask" + page + + case category.Show: + return baseURL + "show" + page + + default: + return "" + } +} diff --git a/utils/strip-ansi/strip-ansi.go b/utils/strip-ansi/strip-ansi.go @@ -0,0 +1,14 @@ +package stripansi + +import ( + "regexp" +) + +const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|" + + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + +func Strip(text string) string { + expression := regexp.MustCompile(ansi) + + return expression.ReplaceAllString(text, "") +}