{"id":39494630,"url":"https://github.com/code-dot-org/redactable-markdown","last_synced_at":"2026-01-18T05:40:57.295Z","repository":{"id":31632048,"uuid":"122247995","full_name":"code-dot-org/redactable-markdown","owner":"code-dot-org","description":"tools for parsing and translating the modified version of markdown used by code.org","archived":false,"fork":false,"pushed_at":"2024-08-31T02:43:23.000Z","size":2401,"stargazers_count":6,"open_issues_count":6,"forks_count":4,"subscribers_count":29,"default_branch":"master","last_synced_at":"2025-06-29T21:01:53.574Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://code-dot-org.github.io/redactable-markdown/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","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":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-02-20T19:51:42.000Z","updated_at":"2025-01-27T02:32:09.000Z","dependencies_parsed_at":"2024-03-25T17:46:32.963Z","dependency_job_id":"d4207e92-bd69-41ea-91f3-fd7a02481c7e","html_url":"https://github.com/code-dot-org/redactable-markdown","commit_stats":{"total_commits":318,"total_committers":8,"mean_commits":39.75,"dds":"0.23584905660377353","last_synced_commit":"a63a122578eca32fa0627a26a2c315a93fd8c97b"},"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/code-dot-org/redactable-markdown","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fredactable-markdown","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fredactable-markdown/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fredactable-markdown/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fredactable-markdown/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/code-dot-org","download_url":"https://codeload.github.com/code-dot-org/redactable-markdown/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/code-dot-org%2Fredactable-markdown/sbom","scorecard":{"id":295597,"data":{"date":"2025-08-11","repo":{"name":"github.com/code-dot-org/redactable-markdown","commit":"a63a122578eca32fa0627a26a2c315a93fd8c97b"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Code-Review","score":6,"reason":"Found 4/6 approved changesets -- score normalized to 6","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":"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":"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":"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/redactable-markdown/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/redactable-markdown/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":"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":"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":"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":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/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/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 28 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":2,"reason":"8 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-w5p7-h5w8-2hfq","Warn: Project is vulnerable to: GHSA-4vvj-4cpr-p986"],"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:55.045Z","repository_id":31632048,"created_at":"2025-08-17T19:24:55.045Z","updated_at":"2025-08-17T19:24:55.045Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28531267,"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":[],"created_at":"2026-01-18T05:40:53.739Z","updated_at":"2026-01-18T05:40:57.290Z","avatar_url":"https://github.com/code-dot-org.png","language":"JavaScript","readme":"# redactable-markdown\n\n[![Build Status](https://github.com/code-dot-org/redactable-markdown/actions/workflows/continuous-integration-tests.yml/badge.svg?branch=master)](https://github.com/code-dot-org/redactable-markdown/actions/workflows/continuous-integration-tests.yml)\n[![npm version](https://img.shields.io/npm/v/@code-dot-org/redactable-markdown.svg)](https://www.npmjs.com/package/@code-dot-org/redactable-markdown)\n\ntools for parsing and translating the modified version of markdown used by code.org\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 and extended CDO Markdown, we add\ntwo new operations: Redaction 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### Other examples\n\n#### Divclass\n\nA new syntax introduced by CDO Markdown, divclasses are a great example of\n\"block\" redactions. Divclasses allow us to wrap bits of content in divs with a\ngiven class, and they look like:\n\n    [some-class-name]\n\n    Inner content\n\n    - content can contain\n    - *whatever* __syntax__ we generally support\n    - Including [things that get redacted](http://example.com)\n\n    [/some-class-name]\n\nThat content would get redacted to:\n\n    [][0]\n\n    Inner content\n\n    -   content can contain\n    -   _whatever_ **syntax** we generally support\n    -   Including [things that get redacted][1]\n\n    [/][0]\n\n#### Vocabulary Definition\n\nA new syntax originally introduced to support CurriculumBuilder (but since\nexpanded to Dashboard's Markdown Preprocessor), Vocabulary Definitions are a\ngreat example of a simple inline redaction with a slightly more complicated\nrestoration process.  Vocablinks are used to embed definitions for complex\nterms within a paragraph, and they look like:\n\n    In particular we are interested in developing a more robust [v protocol/coursea/2021] for sending a list of numbers over the internet.\n\nThat content would get redacted to:\n\n    In particular we are interested in developing a more robust [protocol][0] for sending a list of numbers over the internet.\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/parser/).\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\nindex c87085f..12b67ed 100644\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/redactable-markdown/blob/29c64b3c9e736e28746e6a8627b57c8cc3f0dcc0/src/plugins/process/restorationRegistration.js#L8-L15)):\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\nindex 7a6fc91..08f1bf0 100644\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: 'redaction',\n+        redactionType: 'mention',\n+        name: name,\n+        text: text\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\nindex 08f1bf0..beb01ca 100644\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+  restorationMethods.mention = function (add, node) {\n+    return createMention(add, node.name, node.text);\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```bash\n$ echo \"Hello @example\" \u003e source.md\n$ redact source.md -p mention.js | tee redacted.md\nHello [][0]\n$ sed 's/Hello/Bonjour/' redacted.md | tee translated.md\nBonjour [][0]\n$ restore -s source.md -r translated.md -p mention.js\nBonjour [@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\nindex beb01ca..af09cc5 100644\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         name: name,\n-        text: text\n+        children: [{\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\nindex beb01ca..af09cc5 100644\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-  restorationMethods.mention = function (add, node) {\n-    return createMention(add, node.name, node.text);\n+  restorationMethods.mention = function (add, node, content) {\n+    return createMention(add, node.name, content);\n   }\n\n   /* Make the Parser's redact option visible to the tokenizer */\n```\n\nThe result:\n\n```bash\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```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-dot-org%2Fredactable-markdown","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcode-dot-org%2Fredactable-markdown","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcode-dot-org%2Fredactable-markdown/lists"}