{"id":21654632,"url":"https://github.com/localvoid/rtext","last_synced_at":"2026-05-18T02:31:48.946Z","repository":{"id":57355771,"uuid":"98637561","full_name":"localvoid/rtext","owner":"localvoid","description":":pencil2: Easy-to-use Rich Text in javascript","archived":false,"fork":false,"pushed_at":"2018-05-18T12:26:57.000Z","size":67,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-22T12:54:56.342Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/localvoid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-07-28T10:12:05.000Z","updated_at":"2018-12-01T13:35:30.000Z","dependencies_parsed_at":"2022-09-05T21:30:24.117Z","dependency_job_id":null,"html_url":"https://github.com/localvoid/rtext","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/localvoid/rtext","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localvoid%2Frtext","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localvoid%2Frtext/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localvoid%2Frtext/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localvoid%2Frtext/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/localvoid","download_url":"https://codeload.github.com/localvoid/rtext/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/localvoid%2Frtext/sbom","scorecard":{"id":596686,"data":{"date":"2025-08-18","repo":{"name":"github.com/localvoid/rtext","commit":"627c22ffbcd407be24ce3538dd257e883a13e397"},"scorecard":{"version":"v5.2.1-41-g40576783","commit":"40576783fda6698350fcbbeaea760ff827433034"},"score":1.7,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/8 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#sast"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#cii-best-practices"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#signed-releases"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#fuzzing"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"77 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-6chw-6frg-f759","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-cwfw-4gq5-mrqx","Warn: Project is vulnerable to: GHSA-g95f-p29q-9xw4","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-c6rq-rjc2-86v2","Warn: Project is vulnerable to: GHSA-mh2h-6j8q-x246","Warn: Project is vulnerable to: GHSA-5q88-cjfq-g2mh","Warn: Project is vulnerable to: GHSA-xp63-6vf5-xf3v","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-rq8g-5pc5-wrhr","Warn: Project is vulnerable to: GHSA-9vvw-cc9w-f27h","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-h6ch-v84p-w6p9","Warn: Project is vulnerable to: GHSA-qrmc-fj45-qfc2","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-8r6j-v8pm-fqw3","Warn: Project is vulnerable to: MAL-2023-462","Warn: Project is vulnerable to: GHSA-q42p-pg8m-cqh6","Warn: Project is vulnerable to: GHSA-w457-6q6x-cgp9","Warn: Project is vulnerable to: GHSA-62gr-4qp9-h98f","Warn: Project is vulnerable to: GHSA-f52g-6jhx-586p","Warn: Project is vulnerable to: GHSA-2cf5-4w76-r9qv","Warn: Project is vulnerable to: GHSA-3cqr-58rm-57f8","Warn: Project is vulnerable to: GHSA-g9r4-xpmj-mj65","Warn: Project is vulnerable to: GHSA-q2c6-c6pm-g3gh","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-44pw-h2cw-w3vq","Warn: Project is vulnerable to: GHSA-c429-5p7v-vgjp","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-2pr6-76vf-7546","Warn: Project is vulnerable to: GHSA-8j8c-7jfh-h6hx","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-6c8f-qphg-qjgp","Warn: Project is vulnerable to: GHSA-4xc9-xhrj-v574","Warn: Project is vulnerable to: GHSA-x5rq-j2xg-h7qm","Warn: Project is vulnerable to: GHSA-jf85-cpcp-j695","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-fvqr-27wr-82fm","Warn: Project is vulnerable to: GHSA-4xcv-9jjx-gfj3","Warn: Project is vulnerable to: GHSA-f9cm-qmx5-m98h","Warn: Project is vulnerable to: GHSA-7wpw-2hjm-89gp","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-fhjf-83wg-r2j9","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-6g33-f262-xjp4","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-4g88-fppr-53pp","Warn: Project is vulnerable to: GHSA-4jqc-8m5r-9rpr","Warn: Project is vulnerable to: GHSA-g4rg-993r-mgx7","Warn: Project is vulnerable to: GHSA-3jfq-g458-7qm9","Warn: Project is vulnerable to: GHSA-r628-mhmh-qjhw","Warn: Project is vulnerable to: GHSA-9r2w-394v-53qc","Warn: Project is vulnerable to: GHSA-5955-9wpr-37jh","Warn: Project is vulnerable to: GHSA-qq89-hq3f-393p","Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36","Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/40576783fda6698350fcbbeaea760ff827433034/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-20T23:18:34.660Z","repository_id":57355771,"created_at":"2025-08-20T23:18:34.660Z","updated_at":"2025-08-20T23:18:34.660Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272766915,"owners_count":24989407,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-29T02:00:10.610Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-25T08:28:29.990Z","updated_at":"2026-05-18T02:31:43.904Z","avatar_url":"https://github.com/localvoid.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [rtext](https://github.com/localvoid/rtext) \u0026middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/localvoid/rtext/blob/master/LICENSE) [![codecov](https://codecov.io/gh/localvoid/rtext/branch/master/graph/badge.svg)](https://codecov.io/gh/localvoid/rtext) [![CircleCI Status](https://circleci.com/gh/localvoid/rtext.svg?style=shield\u0026circle-token=:circle-token)](https://circleci.com/gh/localvoid/rtext) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/localvoid/rtext)\n\n|Package      |NPM version                                                                                                |\n|-------------|-----------------------------------------------------------------------------------------------------------|\n|rtext        |[![npm version](https://img.shields.io/npm/v/rtext.svg)](https://www.npmjs.com/package/rtext)              |\n|rtext-render |[![npm version](https://img.shields.io/npm/v/rtext-render.svg)](https://www.npmjs.com/package/rtext-render)|\n|rtext-writer |[![npm version](https://img.shields.io/npm/v/rtext-writer.svg)](https://www.npmjs.com/package/rtext-writer)|\n\n## Core Data Structures\n\nRich text is stored as a simple string with an additional array of annotations.\n\n```ts\ninterface RichText {\n  readonly text: string;\n  readonly annotations?: RichTextAnnotation[];\n}\n```\n\nAnnotations has a `type`, `start` and `end` positions that specify annotated range. Optional `data` property can\nprovide an additional data, and optional `key` property helps with overlapping annotations.\n\n```ts\ninterface RichTextAnnotation\u003cT = any\u003e {\n  readonly type: string;\n  readonly start: number;\n  readonly end: number;\n  readonly data?: T;\n  readonly key?: string;\n}\n```\n\nAll annotations stored in `RichText` should be sorted by their `start` positions. `rtext-writer` package\nautomatically guarantee that rich text composed by this package will be in correct format.\n\nAnd that is all, you are free to choose which types to use and what data to store. With `rtext-render` package it will\nbe easy to convert rich texts into any format you like, for example, colored output in terminal, or HTML in browser.\n\n## Rich Text Writer\n\n`rtext-writer` package provides an easy-to-use interface for composing rich texts. It is using a\n[builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) with a chainable API.\n\n```ts\nclass RichTextWriter {\n  constructor();\n  write(...ws: Array\u003cstring | RichText | ((w: RichTextWriter) =\u003e void)\u003e): this;\n  begin(type: string, data?: any): this;\n  beginKey(key: string, type: string, data?: any): this;\n  end(type: string): this;\n  endKey(key: string, type: string): this;\n  continue(type: string, data?: any): this;\n  continueKey(key: string, type: string, data?: any): this;\n  compose(): RichText;\n}\n```\n\nAPI is pretty simple, `begin()` and `end()` methods are used to specify annotations, `write()` is used to write text and\n`compose()` to get results.\n\n`beginKey()` and `endKey()` are used to create overlapping annotations that have the same `type`. When `endKey()` is\nused, it will try to find an annotation with a matching `type` and `key`.\n\n`continue()` methods will reuse the last annotation when it has the same `end` position and all properties are matching.\n\n### Utility functions\n\n```ts\nfunction richText(): RichTextWriter;\n\nfunction rt(\n  literals: TemplateStringsArray,\n  ...placeholders: Array\u003cstring | RichText | ((w: RichTextWriter) =\u003e void)\u003e,\n): (w: RichTextWriter) =\u003e void;\n\nfunction annotate(text: string, regexp: RegExp, type: string, data?: any, key?: string): RichText;\n```\n\n`richText()` is a simple helper function that will instantiate `RichTextWriter` objects.\n\n`rt()` is a tagged template literal that will create a writer function.\n\n`annotate()` will annotate all text regions matched by `regexp`.\n\n### Example\n\n```ts\nfunction expected(value: any) {\n  return function(w: RichTextWriter) {\n    w.begin(\"expected\").write(JSON.stringify(value)).end(\"expected\");\n  };\n}\n\nfunction received(value: any) {\n  return function(w: RichTextWriter) {\n    w.begin(\"received\").write(JSON.stringify(value)).end(\"received\");\n  };\n}\n\nconst a = { value: 1 };\nconst b = { value: 2 };\n\nconst errorMessage = richText()\n  .begin(\"errorTitle\").write(\"Error Title\\n\").end(\"errorTitle\")\n  .write(\"Expected: \", expected(a), \"\\n\")\n  .write(\"Received: \", received(b), \"\\n\")\n  .compose();\n```\n\n### Example from the iko library\n\n[iko](https://github.com/localvoid/iko) has a high-level API that was built on top of `RichTextWriter`, and it looks\nlike this:\n\n```ts\nexport class NumberAssertion extends Assertion\u003cnumber\u003e {\n  toBeApproximatelyEqual(number: number, epsilon = Number.EPSILON): this {\n    const a = this.obj;\n    const b = number;\n    const aAbs = Math.abs(a);\n    const bAbs = Math.abs(b);\n    const pass = Math.abs(a - b) \u003c= (aAbs \u003c bAbs ? bAbs : aAbs) * epsilon;\n\n    if (!pass) {\n      const message = errMsg()\n        .matcherHint(\"toBeApproximatelyEqual\", \"received\", \"expected\", \"epsilon\")\n        .hint(rt`abs(${rA} - ${eB}) \u003c= (abs(${rA}) \u003c abs(${eB}) ? abs(${eB}) : abs(${rA})) * ${eE}\\n\\n`)\n        .info(rt`Expected number to be approximately equal to ${e(b)}, intstead received ${r(a)}\\n`);\n\n      throw new AssertionError(message.compose(), this.toBeApproximatelyEqual);\n    }\n\n    return this;\n  }\n}\n```\n\n## Rich Text Renderer\n\n`rtext-render` provides a pluggable renderer that deals with all complexities, so you just need to specify several hooks\nand it will be ready to render into any format you like.\n\n```ts\ninterface RichTextRendererHooks\u003cC, R\u003e {\n  onInit(): C;\n  onEnter(annotation: RichTextAnnotation, parentContext: C): C;\n  onExit(annotation: RichTextAnnotation, context: C, parentContext: C): void;\n  onText(text: string, context: C): void;\n  onResult(context: C): R;\n}\n\nfunction createRichTextRenderer\u003cC, R\u003e(\n  priorities: { [key: string]: number },\n  hooks: RichTextRendererHooks\u003cC, R\u003e,\n): (richText: RichText) =\u003e R;\n```\n\n`createRichTextRenderer()` function is used to create a new renderers. It has two parameters, `priorities` to specify\npriorities for annotation types and `hooks` that will be invoked when renderer will process rich text.\n\n### Priorities\n\nPriorities are used to resolve conflicts with overlapping annotations and to make sure that we can generate consistent\nHTML documents.\n\nFor example, when generating HTML anchors, they should have a precedence over `b` elements.\n\n```\nJust some random text with an anchor element\n          [ bold         ]\n                      [ link       ]\n```\n\nWhen converting this rich text to HTML we would like to get HTML like this:\n\n```html\nJust some \u003cb\u003erandom text \u003c/b\u003e\u003ca\u003e\u003cb\u003ewith\u003c/b\u003e an anchor\u003c/a\u003e element\n```\n\n### Hooks\n\n`onInit()` hook is used to initialize an internal state. It should return a root context.\n\n`onEnter()` and `onExit()` hooks will be invoked each time renderer enters or exits an annotation. When renderer enters\nan annotation with a higher priority than annotations in the current context, it will always exit from annotations with\nlower priorities and then re-enter into them. `onEnter()` hook should return a data structure with a current context.\n\n`onText()` hook will be invoked with a current context when text is rendered.\n\n`onResult()` hook should return a result of a renderer from a root context.\n\n### Example\n\nIn this example we will render rich text into a colored output in a terminal with a\n[chalk](https://www.npmjs.com/package/chalk) package.\n\n```js\nimport { createRichTextRenderer } from \"rtext-render\";\nimport * as chalk from \"chalk\";\n\nconst render = createRichTextRenderer(\n  {\n    \"bold\": 1,\n    \"underline\": 2,\n    \"color\": 3,\n    \"bgColor\": 4,\n  },\n  {\n    onInit: () =\u003e ({ chalk, result: \"\" }),\n    onEnter: (a, c) =\u003e {\n      let newChalk = c.chalk;\n      switch (a.type) {\n        case \"bold\":\n          newChalk = newChalk.bold;\n          break;\n        case \"underline\":\n          newChalk = newChalk.underline;\n          break;\n        case \"color\":\n          newChalk = newChalk[a.data];\n          break;\n        case \"bgColor\":\n          const color = `bg${a.data[0].toUpperCase()}${a.data.slice(1)}`;\n          newChalk = newChalk[color];\n          break;\n      }\n      return { chalk: newChalk, result: \"\" };\n    },\n    onExit: (a, c, p) =\u003e {\n      p.result += c.result;\n    },\n    onText: (t, c) =\u003e { c.result += c.chalk(t); },\n    onResult: (c) =\u003e c.result,\n  },\n);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalvoid%2Frtext","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flocalvoid%2Frtext","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocalvoid%2Frtext/lists"}