diff --git a/.github/workflows/issues-jira.yml b/.github/workflows/issues-jira.yml
new file mode 100644
index 0000000..7bf0469
--- /dev/null
+++ b/.github/workflows/issues-jira.yml
@@ -0,0 +1,31 @@
+name: Create Jira Ticket for Github Issue
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ issue-jira:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Login to Jira
+ uses: atlassian/gajira-login@master
+ env:
+ JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
+ JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
+ JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
+
+ - name: Create Jira Issue
+ id: create_jira
+ uses: atlassian/gajira-create@master
+ with:
+ project: ${{ secrets.JIRA_PROJECT }}
+ issuetype: ${{ secrets.JIRA_ISSUE_TYPE }}
+ summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }}
+ description: |
+ *GitHub Issue:* ${{ github.event.issue.html_url }}
+
+ *Description:*
+ ${{ github.event.issue.body }}
+ fields: "${{ secrets.ISSUES_JIRA_FIELDS }}"
\ No newline at end of file
diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml
deleted file mode 100644
index 250abc7..0000000
--- a/.github/workflows/jira.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Create JIRA ISSUE
-on:
- pull_request:
- types: [opened]
-jobs:
- security-jira:
- if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'snyk-bot' || contains(github.event.pull_request.head.ref, 'snyk-fix-') || contains(github.event.pull_request.head.ref, 'snyk-upgrade-')}}
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Login into JIRA
- uses: atlassian/gajira-login@master
- env:
- JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
- JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
- JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
- - name: Create a JIRA Issue
- id: create
- uses: atlassian/gajira-create@master
- with:
- project: ${{ secrets.JIRA_PROJECT }}
- issuetype: ${{ secrets.JIRA_ISSUE_TYPE }}
- summary: |
- Snyk | Vulnerability | ${{ github.event.repository.name }} | ${{ github.event.pull_request.title }}
- description: |
- PR: ${{ github.event.pull_request.html_url }}
-
- fields: "${{ secrets.JIRA_FIELDS }}"
- - name: Transition issue
- uses: atlassian/gajira-transition@v3
- with:
- issue: ${{ steps.create.outputs.issue }}
- transition: ${{ secrets.JIRA_TRANSITION }}
diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml
new file mode 100644
index 0000000..ff25923
--- /dev/null
+++ b/.github/workflows/policy-scan.yml
@@ -0,0 +1,46 @@
+name: Checks the security policy and configurations
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+jobs:
+ security-policy:
+ if: github.event.repository.visibility == 'public'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@master
+ - name: Checks for SECURITY.md policy file
+ run: |
+ if ! [[ -f "SECURITY.md" || -f ".github/SECURITY.md" ]]; then exit 1; fi
+ security-license:
+ if: github.event.repository.visibility == 'public'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@master
+ - name: Checks for License file
+ run: |
+ expected_license_files=("LICENSE" "LICENSE.txt" "LICENSE.md" "License.txt")
+ license_file_found=false
+ current_year=$(date +"%Y")
+
+ for license_file in "${expected_license_files[@]}"; do
+ if [ -f "$license_file" ]; then
+ license_file_found=true
+ # check the license file for the current year, if not exists, exit with error
+ if ! grep -q "$current_year" "$license_file"; then
+ echo "License file $license_file does not contain the current year."
+ exit 2
+ fi
+ break
+ fi
+ done
+
+ if [ "$license_file_found" = false ]; then
+ echo "No license file found. Please add a license file to the repository."
+ exit 1
+ fi
\ No newline at end of file
diff --git a/.github/workflows/sast-scan.yml b/.github/workflows/sast-scan.yml
deleted file mode 100644
index 3b9521a..0000000
--- a/.github/workflows/sast-scan.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: SAST Scan
-on:
- pull_request:
- types: [opened, synchronize, reopened]
-jobs:
- security-sast:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Semgrep Scan
- run: docker run -v /var/run/docker.sock:/var/run/docker.sock -v "${PWD}:/src" returntocorp/semgrep semgrep scan --config auto
\ No newline at end of file
diff --git a/.github/workflows/sca-scan.yml b/.github/workflows/sca-scan.yml
index f09161f..2307d48 100644
--- a/.github/workflows/sca-scan.yml
+++ b/.github/workflows/sca-scan.yml
@@ -13,3 +13,6 @@ jobs:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --all-projects --fail-on=all
+ json: true
+ continue-on-error: true
+ - uses: contentstack/sca-policy@main
diff --git a/.talismanrc b/.talismanrc
index 3fd67ab..77ed317 100644
--- a/.talismanrc
+++ b/.talismanrc
@@ -1,4 +1,7 @@
fileignoreconfig:
+- filename: .github/workflows/secrets-scan.yml
+ ignore_detectors:
+ - filecontent
- filename: README.md
checksum: 4bffa07e1e88d50a311464c9f90ff92bcba7fe434be4d9374bcd49bf76b1e6a8
- filename: coverage/lcov-report/base.css
@@ -21,3 +24,12 @@ fileignoreconfig:
checksum: 3d014702628ad538065c970d988a695af003c61663596a8f6b9267b4e57ef6ea
- filename: test/expectedJson.json
checksum: 9979f84be3e5aa27f24381a0c49e0e6696388d19615c4f3b09082780968236ee
+- filename: README.md
+ checksum: cccb3cd93c499acc87593eca5cc032e256c11cf530d4de67ece09e57fc430215
+- filename: test/expectedJson.ts
+ checksum: a1966b0b3993c8e3a0e9e45de49204e7788ba74ba0089a8a6b6eba0729f990bd
+- filename: package-lock.json
+ checksum: 96da2dcdb517a744b09062fad7fbe38f49e4efe5535a3e9e65a94805e7e4808c
+- filename: src/toRedactor.tsx
+ checksum: c6792b5b19cf89024ab33333a77d22af0375d4976c2fc64dae1d2f5971493397
+version: "1.0"
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index 1be7e0d..49adcb9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1,11 @@
-* @contentstack/security-admin
+* @contentstack/venus-pr-reviewers
+
+.github/workflows/sca-scan.yml @contentstack/security-admin
+
+.github/workflows/codeql-anaylsis.yml @contentstack/security-admin
+
+**/.snyk @contentstack/security-admin
+
+.github/workflows/policy-scan.yml @contentstack/security-admin
+
+.github/workflows/issues-jira.yml @contentstack/security-admin
diff --git a/LICENSE b/LICENSE
index 46147ae..ecd01a2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023-2024 Contentstack
+Copyright (c) 2021-2026 Contentstack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 5921e5d..ef93d64 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,28 @@ On the other hand, the `customTextWrapper` parser function provides the followin
- `child`: The HTML string that specifies the child element
- `value`: The value passed against the child element
+___
+
+ `allowedEmptyAttributes`
+
+- Type: `object`
+- Default: `{ img: ['alt'], reference: ['alt'] }`
+
+Specifies which empty attributes should be retained for specific HTML elements during the jsonToHtml conversion.
+By default, the converter preserves the alt attribute for and reference (asset) elements, even when their values are empty.
+This is particularly useful for ensuring semantic correctness and accessibility.
+
+Use this option when you want to retain specific attributes with empty values during the conversion process.
+
+___
+
+ `addNbspForEmptyBlocks`
+
+- Type: `boolean`
+- Default:`false`
+
+When set to true, this option adds a non-breaking space (nbsp;) to empty blocks during the jsonToHtml conversion. This helps maintain the visual structure of the HTML output—especially useful for preserving spacing in editable content or content editors.
+
You can use the following customized JSON RTE Serializer code to convert your JSON RTE field data into HTML format.
```javascript
@@ -189,6 +211,16 @@ const jsonValue = {
},
],
},
+ {
+ "type": "p",
+ "uid": "28c837c127504d3c85b9cb6d7099cb0b",
+ "attrs": {},
+ "children": [
+ {
+ "text": ""
+ }
+ ]
+ },
{
type: "p",
attrs: {},
@@ -215,6 +247,11 @@ const htmlValue = jsonToHtml(
return `${child}`;
},
},
+ allowedEmptyAttributes : {
+ "p": ["dir"],
+ "img" : ["width"]
+ },
+ addNbspForEmptyBlocks : true
}
);
@@ -356,12 +393,43 @@ The resulting JSON-formatted data will look as follows:
## Automatic Conversion
+> **_Note_**: `src` url's provided for social-embeds and embed items will by default be uri encoded.
+
By default, the JSON Rich Text Editor field supports limited HTML tags within the editor. Due to this, the JSON RTE Serializer tool is not able to recognize each and every standard HTML tag.
To help the JSON RTE Serializer recognize and process additional tags that are commonly used across HTML, you can use the automatic conversion option. When using this option, you need to pass the `allowNonStandardTags: true` parameter within the `jsonToHtml` or `htmlToJson` method to manipulate the working of the JSON RTE Serializer package as per your requirements. When you pass this parameter, it customizes your JSON RTE Serializer code to allow the support for all standard HTML-recognized tags or element types in the JSON Rich Text Editor field.
### Convert JSON to HTML
+#### HTML Attribute Name and Value Sanitization
+
+
+This project ensures that HTML attributes are properly validated and sanitized according to the W3C HTML specification. It validates attribute names based on the HTML standards and sanitizes attribute values to ensure correct rendering and security, particularly against cross-site scripting (XSS) vulnerabilities.
+
+#### Attribute Name Guidelines
+
+All HTML attribute names must conform to the [W3C HTML specification](https://www.w3.org/TR/2012/WD-html-markup-20120329/syntax.html#attribute-name). These guidelines specify the following rules:
+
+- **Printable ASCII Characters:** Attribute names must consist only of printable ASCII characters.
+- **Case-Insensitive:** Attribute names are case-insensitive, but lowercase is preferred for consistency.
+- **No Special Characters:** Attribute names cannot contain spaces or special characters such as `=`, `>`, `<`, `"`, etc.
+- **Allowed Attributes:** Attributes such as `xmlns`, `aria-*`, `data-*`, and others defined by HTML5 standards are allowed and must follow specific rules.
+
+##### Important Note:
+If an attribute name does not conform to these rules, the attribute will be **dropped** from the element.
+
+#### Attribute Value Guidelines
+
+The values of HTML attributes are sanitized to ensure proper rendering and to mitigate security risks, such as Cross-Site Scripting (XSS). This sanitization process involves replacing HTML entities (like `<`, `>`, `&`, etc.) with their corresponding characters and removing any invalid or unsafe characters.
+
+Here are some common HTML entities and their replacements:
+
+- `<` → `<`
+- `>` → `>`
+- `&` → `&`
+
+
+
You can pass the `allowNonStandardTags: true` parameter within the `jsonToHtml` method to allow the JSON RTE Serializer tool to recognize standard HTML tags or element types and convert them into JSON format.
You can use the following customized JSON RTE Serializer code to convert your JSON RTE field data into HTML format.
@@ -542,6 +610,129 @@ The resulting JSON-formatted data will look as follows:
}
```
+## Async JSON to HTML Conversion
+
+For scenarios where custom element handlers need to perform asynchronous operations (e.g., resolving dynamic imports, fetching data), use `jsonToHtmlAsync`:
+
+```typescript
+import { jsonToHtmlAsync } from "@contentstack/json-rte-serializer";
+
+const html = await jsonToHtmlAsync(jsonValue, {
+ customElementTypes: {
+ // Sync handlers still work
+ p: (attrs, child) => `
${child}
`,
+
+ // Async handlers are now supported
+ reference: async (attrs, child, jsonBlock) => {
+ const mod = await import(`./components/${jsonBlock.attrs.type}`);
+ return renderToStaticMarkup();
+ },
+ },
+});
+```
+
+`jsonToHtmlAsync` has the same API as `jsonToHtml` — the only difference is that handler return values are `await`ed, so both `string` and `Promise` work. Children are resolved concurrently via `Promise.all`.
+
+## Generic Tree Walker — `toTree()`
+
+For consumers who need output other than HTML strings (React elements, Preact vnodes, Vue render functions, etc.), `toTree()` provides a framework-agnostic tree walker. You supply the construction callbacks; the walker handles recursion, text marks, and line breaks.
+
+```typescript
+import { toTree, IJsonToTreeOptions } from "@contentstack/json-rte-serializer";
+
+const options: IJsonToTreeOptions = {
+ // Required: map element types to your output format
+ elementTypes: {
+ p: (jsonBlock, children) => myCreateElement("p", children),
+ h1: (jsonBlock, children) => myCreateElement("h1", children),
+ a: (jsonBlock, children) => myCreateElement("a", { href: jsonBlock.attrs?.url }, children),
+ // ...add handlers for each type you need
+ },
+
+ // Required: how to create a text node in your output format
+ createText: (text) => text,
+
+ // Required: how to create a line break
+ createLineBreak: (key) => myCreateElement("br", { key }),
+
+ // Required: how to combine multiple children into one
+ combineChildren: (children) => myCreateFragment(children),
+
+ // Optional: text mark wrappers (bold, italic, etc.)
+ textMarks: {
+ bold: (children) => myCreateElement("strong", children),
+ italic: (children) => myCreateElement("em", children),
+ },
+
+ // Optional: wrap text nodes that have classname/id attrs
+ wrapTextAttrs: (node, { classname, id }) =>
+ myCreateElement("span", { className: classname, id }, node),
+
+ // Optional: wrap text nodes with inline styles
+ wrapTextStyle: (node, style) =>
+ myCreateElement("span", { style }, node),
+
+ // Optional: assign a key to an element (for keyed lists in virtual DOM frameworks)
+ keyElement: (element, key) => myAssignKey(element, key),
+};
+
+const result = toTree(jsonRteDocument, options);
+```
+
+### React Reference Implementation (`/react`)
+
+This package includes a ready-to-use React entry point at `@contentstack/json-rte-serializer/react` that implements all the callbacks above for React. It exports:
+
+- **`reactPrimitives`** — the `createText`, `createLineBreak`, `combineChildren`, `wrapTextAttrs`, `wrapTextStyle`, and `keyElement` callbacks for React
+- **`defaultElementTypes`** — handlers for all standard JSON RTE element types (`p`, `h1`–`h6`, `a`, `img`, `table`, lists, grid, etc.)
+- **`defaultTextMarks`** — handlers for bold, italic, underline, strikethrough, superscript, subscript, inlineCode
+- **`jsonToReact()`** — convenience wrapper that combines all of the above
+
+```tsx
+import { jsonToReact } from "@contentstack/json-rte-serializer/react";
+
+// Basic usage — renders all standard types out of the box
+const content = jsonToReact(jsonRteDocument);
+
+// With custom overrides
+const content = jsonToReact(jsonRteDocument, {
+ customElementTypes: {
+ // Override specific handlers — merged on top of defaults
+ reference: (jsonBlock, children) => {
+ return ;
+ },
+ a: (jsonBlock, children) => {
+ return {children};
+ },
+ },
+ customTextMarks: {
+ // Add custom text marks or override defaults
+ highlight: (children) => {children},
+ },
+});
+
+// Render directly — no dangerouslySetInnerHTML needed
+return