{"id":39493758,"url":"https://github.com/code-dot-org/remark-redactable","last_synced_at":"2026-01-18T05:39:23.061Z","repository":{"id":33706768,"uuid":"160886463","full_name":"code-dot-org/remark-redactable","owner":"code-dot-org","description":"remark plugin to enable other plugins for redaction and restoration of content","archived":false,"fork":false,"pushed_at":"2022-02-11T13:37:22.000Z","size":533,"stargazers_count":6,"open_issues_count":5,"forks_count":1,"subscribers_count":25,"default_branch":"master","last_synced_at":"2025-08-09T14:59:54.696Z","etag":null,"topics":["javascript","markdown","remark","remark-plugin"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/code-dot-org.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-07T23:42:48.000Z","updated_at":"2025-01-28T03:56:52.000Z","dependencies_parsed_at":"2022-08-07T23:00:29.968Z","dependency_job_id":null,"html_url":"https://github.com/code-dot-org/remark-redactable","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/code-dot-org/remark-redactable","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fremark-redactable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fremark-redactable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fremark-redactable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fremark-redactable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/code-dot-org","download_url":"https://codeload.github.com/code-dot-org/remark-redactable/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fremark-redactable/sbom","scorecard":{"id":295599,"data":{"date":"2025-08-11","repo":{"name":"github.com/code-dot-org/remark-redactable","commit":"4fd9738bf80f4667a9084eccd819d6f1734d937c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.8,"checks":[{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/continuous-integration-tests.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":3,"reason":"dependency not pinned by hash detected -- score normalized to 3","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/continuous-integration-tests.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/code-dot-org/remark-redactable/continuous-integration-tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/continuous-integration-tests.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/code-dot-org/remark-redactable/continuous-integration-tests.yml/master?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   1 out of   1 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Code-Review","score":4,"reason":"Found 5/12 approved changesets -- score normalized to 4","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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"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/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 23 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"16 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-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","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-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-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-w5p7-h5w8-2hfq"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T19:24:56.181Z","repository_id":33706768,"created_at":"2025-08-17T19:24:56.182Z","updated_at":"2025-08-17T19:24:56.182Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28531160,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T00:39:45.795Z","status":"online","status_checked_at":"2026-01-18T02:00:07.578Z","response_time":98,"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":["javascript","markdown","remark","remark-plugin"],"created_at":"2026-01-18T05:39:22.971Z","updated_at":"2026-01-18T05:39:23.055Z","avatar_url":"https://github.com/code-dot-org.png","language":"JavaScript","readme":"# remark-redactable\n\n[![Build Status](https://github.com/code-dot-org/remark-redactable/actions/workflows/continuous-integration-tests.yml/badge.svg?branch=master)](https://github.com/code-dot-org/remark-redactable/actions/workflows/continuous-integration-tests.yml)\n[![npm version](https://img.shields.io/npm/v/remark-redactable.svg)](https://www.npmjs.com/package/remark-redactable)\n\nA plugin that allows sensitive information or complex syntax in markdown\ndocuments to be removed (\"redacted\") from the document, then reattached\n(\"restored\") to the document at some later point.\n\nUsed by [code.org](https://code.org/) via\n[`redactable-markdown`](https://github.com/code-dot-org/redactable-markdown) to\nenable better internationalization of sensitive, controlled, or complex\ncontent.\n\n## Usage\n\nThis example redacts a source string, then restores a translated version of\nthat redaction with the original content from the source string.\n\n```javascript\nconst remark = require('remark')\nconst { redact, restore, plugins } = require('remark-redactable');\n\nconst sourceText = \"A [black](http://black.com) [cat](http://cat.com)\\n\";\nconsole.log(`source: ${sourceText}`);\n\nconst parsedRedactedSourceTree = remark()\n  .use(redact)\n  .use(plugins.redactedLink)\n  .parse(sourceText);\n\nconst transformedRedactedSourceTree = remark()\n  .use(redact)\n  .runSync(parsedRedactedSourceTree);\n\nconst redactedText = remark()\n  .use(redact)\n  .stringify(transformedRedactedSourceTree);\n\nconsole.log(`redacted: ${redactedText}`);\nconst translatedText = \"Une [chat][1] [noir][0]\\n\";\nconsole.log(`translated redacted: ${translatedText}`);\n\nconst restoredText = remark()\n  .use(restore(redactedSourceTree))\n  .use(plugins.redactedLink)\n  .processSync(translatedText).contents;\n\nconsole.log(`restored translation: ${restoredText}`);\n```\n\nYields\n\n```\nsource: A [black](http://black.com) [cat](http://cat.com)\n\nredacted: A [black][0] [cat][1]\n\ntranslated redacted: Une [chat][1] [noir][0]\n\nrestored translation: Une [chat](http://cat.com) [noir](http://black.com)\n```\n\n## Overview\n\nThe standard operation that can be done on a piece of markdown content is\nRendering; the act of parsing the markdown content into an understandable\nstructure and compiling that structure out to (usually) HTML.\n\nTo facilitate better translation of markdown, we add two new operations:\nRedaction and Restoration\n\n## Redaction\n\nRedaction is the process of parsing markdown content into an understandable\nform, then compiling that structure back out to markdown with some values\nremoved and some syntaxes simplified.\n\nFor example, standard markdown links and images:\n\n    [a link](http://example.com)\n    ![an image](http://example.com/img.jpg)\n\nHave their url and href values removed in the redaction process, and in the case\nof images the special `!` character is also removed; simplifying them to just:\n\n    [a link][0]\n    [an image][1]\n\nThe result is that translators are exposed to just those parts of the original\ncontent that we actually want them to translate. This means on our end that we\ncan do much less work to verify that translators are not breaking anything or\nintroducing malicious content, and on the translator's end it means they need to\nworry much less about trying to determine which parts of the string they should\nand should not be responsible for changing.\n\nIn general, content is always redacted to two sets of square brackets, the first\nenclosing whatever english text we want to expose to the translators and the\nsecond enclosing a unique numeric ID we use to associate the redacted content\nback with the original data during the restoration process.\n\n## Restoration\n\nAfter redacting content and sending the redacted content out to be translated,\nwe will get back a translated version of the redacted content. We then combine\nthat with the original content to create a restored translated version of the\noriginal content.\n\nFor example, standard markdown links and images:\n\n    [a link](http://example.com)\n    ![an image](http://example.com/img.jpg)\n\nAfter getting redacted and translated, might come back looking like:\n\n    [un linke][0]\n    [une image][1]\n\nAnd would then be recombined with the original content to produce:\n\n    [un linke](http://example.com)\n    ![une image](http://example.com/img.jpg)\n\nNote that the unique identifiers for each piece of redacted content allow us to\nhandle any reordering that might be introduced by the translation process. For\nexample,\n\n    A [black](http://example.com/black) [cat](http://example.com/cat)\n\nWould be redacted to\n\n    A [black][0] [cat][1]\n\nThen translated to\n\n    Un [chat][1] [noir][0]\n\nThen restored to\n\n    Un [chat](http://example.com/cat) [noir](http://example.com/black)\n\n## Plugins\n\nTo define redaction and restoration functionality for a new or existing piece of\nsyntax, simply create a plugin. Plugins start as remark-parse plugins of the\nform described in [remark-parse Extending the\nParser](https://github.com/remarkjs/remark/tree/master/packages/remark-parse#extending-the-parser),\nand examples can be found [in the source tree](/src/plugins/).\n\n### Basic Redaction Example\n\nFor example, let's add redaction to the `mention` plugin in the remark-parse\nexample. We start with `mention.js` from that example:\n\n```javascript\nmodule.exports = mentions;\n\nfunction mentions() {\n  var Parser = this.Parser;\n  var tokenizers = Parser.prototype.inlineTokenizers;\n  var methods = Parser.prototype.inlineMethods;\n\n  /* Add an inline tokenizer (defined in the following example). */\n  tokenizers.mention = tokenizeMention;\n\n  /* Run it just before `text`. */\n  methods.splice(methods.indexOf('text'), 0, 'mention');\n}\n\ntokenizeMention.notInLink = true;\ntokenizeMention.locator = locateMention;\n\nfunction tokenizeMention(eat, value, silent) {\n  var match = /^@(\\w+)/.exec(value);\n\n  if (match) {\n    if (silent) {\n      return true;\n    }\n\n    return eat(match[0])({\n      type: 'link',\n      url: 'https://social-network/' + match[1],\n      children: [{type: 'text', value: match[0]}]\n    });\n  }\n}\n\nfunction locateMention(value, fromIndex) {\n  return value.indexOf('@', fromIndex);\n}\n```\n\nFirst, isolate the logic that extracts meaningful data from the parsed token\nfrom the logic that builds a node from that extracted data:\n\n```diff\ndiff --git a/mention.js b/mention.js\n--- a/mention.js\n+++ b/mention.js\n@@ -23,14 +29,19 @@ function tokenizeMention(eat, value, silent) {\n       return true;\n     }\n\n-    return eat(match[0])({\n-      type: 'link',\n-      url: 'https://social-network/' + match[1],\n-      children: [{type: 'text', value: match[0]}]\n-    });\n+    var add = eat(match[0]);\n+    return createMention(add, match[1], match[0]);\n   }\n }\n\n function locateMention(value, fromIndex) {\n   return value.indexOf('@', fromIndex);\n }\n+\n+function createMention(add, name, text) {\n+  return add({\n+    type: 'link',\n+    url: 'https://social-network/' + name,\n+    children: [{type: 'text', value: text}]\n+  });\n+}\n```\n\nThen, conditionally create a `redaction` node instead of the desired regular\nnode when in redaction mode (see more about the `redaction` node\n[here](https://github.com/code-dot-org/remark-redactable/blob/master/docs/mdast-nodes.md)):\n\nNote here that all that is required of the redaction node is that it contains a\nunique `redactionType` identifier, and any information required to recreate the\nnode.\n\n```diff\ndiff --git a/mention.js b/mention.js\n--- a/mention.js\n+++ b/mention.js\n@@ -1,10 +1,15 @@\n module.exports = mentions;\n\n+var redact;\n+\n function mentions() {\n   var Parser = this.Parser;\n   var tokenizers = Parser.prototype.inlineTokenizers;\n   var methods = Parser.prototype.inlineMethods;\n\n+  /* Make the Parser's redact option visible to the tokenizer */\n+  redact = Parser.prototype.options.redact;\n+\n   /* Add an inline tokenizer (defined in the following example). */\n   tokenizers.mention = tokenizeMention;\n\n@@ -24,7 +29,19 @@ function tokenizeMention(eat, value, silent) {\n     }\n\n     var add = eat(match[0]);\n-    return createMention(add, match[1], match[0]);\n+    var name = match[1];\n+    var text = match[0];\n+\n+    if (redact) {\n+      return add({\n+        type: 'inlineRedaction',\n+        redactionType: 'mention',\n+        redactionData: {\n+          name: name,\n+          text: text\n+        }\n+      });\n+    }\n+\n+    return createMention(add, name, text);\n   }\n }\n```\n\nFinally, add a restoration method for the specified redaction type, using the\nnewly-isolated node creation logic.\n\n```diff\ndiff --git a/mention.js b/mention.js\n--- a/mention.js\n+++ b/mention.js\n@@ -6,6 +6,11 @@ function mentions() {\n   var Parser = this.Parser;\n   var tokenizers = Parser.prototype.inlineTokenizers;\n   var methods = Parser.prototype.inlineMethods;\n+  var restorationMethods = Parser.prototype.restorationMethods;\n+\n+  if (restorationMethods) {\n+    restorationMethods.mention = function (add, node) {\n+      return createMention(add, node.redactionData.name, node.redactionData.text);\n+    }\n+  }\n\n   /* Make the Parser's redact option visible to the tokenizer */\n   redact = Parser.prototype.options.redact;\n```\n\nWe can now redact and restore `@` mentions:\n\n```javascript\nconst remark = require('remark')\nconst { redact, restore } = require('remark-redactable');\nconst mention = require('mention');\n\nconst sourceText = \"Hello @example\";\n\nconst redactedSourceTree = remark()\n  .use(redact)\n  .use(mention)\n  .parse(sourceText);\n\nconst redactedText = remark()\n  .use(redact)\n  .stringify(redactedSourceTree); // \"Hello [][0]\"\n\nconst translatedText = redactedText\n  .replace(\"Hello\", \"Bonjour\"); // \"Bonjour [][0]\"\n\nconst restoredText = remark()\n  .use(restore(redactedSourceTree))\n  .use(plugins.redactedLink)\n  .processSync(translatedText)\n  .contents; // \"Bonjour [@example](https://social-network/example)\"\n```\n\n### Advanced Redaction Example\n\nWe also have the option of allowing the redaction and restoration process to\nchange the way the parsed text is processed.\n\nSay we wanted the redacted version of the basic example to expose the `@` name\nlike:\n\n    Hello [@example][0]\n\nAnd for changes made to the text in the redaction to be reflected in the\ngenerated link like:\n\n    Bonjour [@exemple][0] \u003e Bonjour [@exemple](https://social-network/example)\n\nTo achieve that, we first move the `text` value from a property on the\n`redaction` node to a full `text` child node:\n\n```diff\ndiff --git a/mention.js b/mention.js\n--- a/mention.js\n+++ b/mention.js\n@@ -42,7 +42,11 @@ function tokenizeMention(eat, value, silent) {\n         type: 'redaction',\n         redactionType: 'mention',\n         redactionData: {\n           name: name,\n-          text: text\n         },\n+        redactionContent: [{\n+          type: 'text',\n+          value: text\n+        }]\n       });\n     }\n```\n\nThen, we expand the restoration method to make use of the optional `content`\nargument, which will contain the modified version of the text content.\n\n```diff\ndiff --git a/mention.js b/mention.js\n--- a/mention.js\n+++ b/mention.js\n@@ -8,8 +8,8 @@ function mentions() {\n   var methods = Parser.prototype.inlineMethods;\n   var restorationMethods = Parser.prototype.restorationMethods;\n\n   if (restorationMethods) {\n-    restorationMethods.mention = function (add, node) {\n-      return createMention(add, node.redactionData.name, node.redactionData.text);\n+    restorationMethods.mention = function (add, node, content) {\n+      return createMention(add, node.redactionData.name, content);\n     }\n   }\n\n   /* Make the Parser's redact option visible to the tokenizer */\n```\n\nThe result:\n\n```javascript\n$ echo \"Hello @example\" \u003e source.md\n$ redact source.md -p mention.js | tee redacted.md\nHello [@example][0]\n$ sed -e 's/Hello/Bonjour/' -e 's/example/exemple/' redacted.md | tee translated.md\nBonjour [@exemple][0]\n$ restore -s source.md -r translated.md -p mention.js\nBonjour [@exemple](https://social-network/example)\n\nconst remark = require('remark')\nconst { redact, restore } = require('remark-redactable');\nconst mention = require('mention');\n\nconst sourceText = \"Hello @example\";\n\nconst parsedRedactedSourceTree = remark()\n  .use(redact)\n  .use(mention)\n  .parse(sourceText);\n\nconst transformedRedactedSourceTree = remark()\n  .use(redact)\n  .runSync(parsedRedactedSourceTree);\n\nconst redactedText = remark()\n  .use(redact)\n  .stringify(transformedRedactedSourceTree); // \"Hello [@example][0]\"\n\nconst translatedText = redactedText\n  .replace(\"Hello\", \"Bonjour\")\n  .replace(\"example\", \"exemple\"); // \"Bonjour [@exemple][0]\"\n\nconst restoredText = remark()\n  .use(restore(redactedSourceTree))\n  .use(plugins.redactedLink)\n  .processSync(translatedText)\n  .contents; // \"Bonjour [@exemple](https://social-network/example)\"\n\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-dot-org%2Fremark-redactable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcode-dot-org%2Fremark-redactable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-dot-org%2Fremark-redactable/lists"}