Repository: Bridgeconn/usfm-grammar (language: python)

Issue:
Replicate python parser in javascript
The usfm-grammar implementation is now done in python, in the module `python-usfm-parser`. Replicate the same functionalities into javascript and make the `javascript-usfm-parser` module


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index d42a9d19..4154bcda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,4 +81,6 @@ tree-sitter-usfm3/src/
 */dist/*
 */src/usfm_grammar.egg-info/*
 **/my-languages.so
-**/wheelhouse/
\ No newline at end of file
+**/wheelhouse/
+js-usfm-parser/src/web-tree-sitter/tree-sitter.js
+js-usfm-parser/src/web-tree-sitter/tree-sitter.wasm
diff --git a/docs/Dev_notes.md b/docs/Dev_notes.md
index e1f8ae95..b8ac58bc 100644
--- a/docs/Dev_notes.md
+++ b/docs/Dev_notes.md
@@ -3,14 +3,14 @@
 ## How to build the binary for python module?
 
 First compile the grammar
-```
+```bash
 cd tree-sitter-usfm3
 export PATH=$PATH:./node_modules/.bin
 tree-sitter generate
 tree-sitter test
 ```
 To use the grammar module still in developement from within the py-usfm-grammar module
-```
+```bash
 cd py-usfm-parser
 source ENV-dev/bin/actiavte
 pip install ../tree-sitter-usfm3
@@ -21,7 +21,7 @@ To make the changes reflect automatically `pip install -e ../tree-sitter-usfm3`.
 ## How to change version number in files?
 
 In python module,
-```
+```bash
 cd usfm-grammar
 source py-usfm-parser/ENV-dev/bin/activate
 bumpversion --new-version 3.0.0-alpha.28 num
@@ -31,7 +31,7 @@ The github action is configured to automatically build and publish to PyPI and N
 
 ## Run tests
 To check Syntax trees in Grammar module
-```
+```bash
 cd tree-sitter-usfm3
 export PATH=$PATH:./node_modules/.bin
 tree-sitter generate
@@ -40,7 +40,7 @@ tree-sitter test
 
 In python module alone
 
-```
+```bash
 cd py-usfm-parser
 python -m pytest -n auto
 
@@ -52,26 +52,33 @@ pytest -k "not compare_usx_with_testsuite_samples and not testsuite_usx_with_rnc
 ## How to build and publish JS module for local Development
 
 First compile the grammar and get the wasm file
-```
+```bash
 cd tree-sitter-usfm3
 export PATH=$PATH:./node_modules/.bin
 tree-sitter generate
 cp tree-sitter-usfm.wasm ../js-usfm-parser/
+cd ..
 ```
-After npm install, copy the `tree-sitter.wasm` file from `node_modules/web=tree-sitter` to the `js-usfm-parser` folder to include it with the npm packaging.
-
+After npm install, copy the `tree-sitter.js` file from `node_modules/web-tree-sitter` to the `js-usfm-parser/src/web-tree-sitter` folder to include it in the bundle. Also copy the `tree-sitter.wasm` file to `js-usfm-parser/` to be included in the npm packaging.
 
-Build the code base generating both cjs and esm versions of the same code base. The configs are in `.babelrc` file. Upon running the commands two folders `dist/cjs/` and `dist/esm` would be created.
+```bash
+cd js-usfm-parser/
+npm install .
+cp node_modules/web-tree-sitter/tree-sitter.js src/web-tree-sitter/
+cp node_modules/web-tree-sitter/tree-sitter.wasm ./
 
 ```
-cd ../js-usfm-parser
+
+Build the code base generating both cjs and esm versions of the same code base. This used parcel and its configs are in package.json(main, module, source, etc). Upon running the commands two folders `dist/cjs/` and `dist/esm` would be created.
+
+```bash
 rm -fr ./dist
 npm run build
 ```
 
 Use  a local publishing registry for local development and testing
 
-```
+```bash
 npm install -g verdaccio # need not do again if done once
 verdaccio # runs a server at localhost:4873
 touch .npmrc
diff --git a/docs/react-usage.md b/docs/react-usage.md
new file mode 100644
index 00000000..827b7e20
--- /dev/null
+++ b/docs/react-usage.md
@@ -0,0 +1,36 @@
+## How to use the usfm-grammar npm package from React
+
+To use the library from a react app, there are a few extra handling required.
+1. The modules `fs`, `path` and `process` used by tree-sitter is not required when using from front end. But bundling may cause issue. Hence use `react-app-rewired` and in the `config-overrides.js` file add following settings:
+
+ ```javascript
+  const { override } = require('customize-cra');
+
+  module.exports = override(
+    config => {
+      config.resolve.fallback = {
+        fs: false,
+        path: false,
+        process: false
+      };
+      return config;
+    },
+  );
+ ```
+2. When initializing the `USFMParser` class use the links to required wasm files as shown below:
+```javascript
+import React, { useEffect } from 'react';
+import { USFMParser } from 'usfm-grammar';
+
+function App() {
+  ...
+  useEffect(() => {
+    const initParser = async () => {
+      await USFMParser.init("https://cdn.jsdelivr.net/npm/usfm-grammar@3.0.0-alpha.6/tree-sitter-usfm.wasm",
+                            "https://cdn.jsdelivr.net/npm/usfm-grammar@3.0.0-alpha.6/tree-sitter.wasm");
+    };
+    initParser();
+  }, []);
+  ...
+}
+```
\ No newline at end of file
diff --git a/js-usfm-parser/.babelrc b/js-usfm-parser/.babelrc
deleted file mode 100644
index b2a27e58..00000000
--- a/js-usfm-parser/.babelrc
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "env": {
-    "cjs": {
-      "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
-    },
-    "esm": {
-      "presets": [["@babel/preset-env", { "modules": false }]]
-    }
-  }
-}
diff --git a/js-usfm-parser/.prettierignore b/js-usfm-parser/.prettierignore
deleted file mode 100644
index 00446809..00000000
--- a/js-usfm-parser/.prettierignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/node_modules
-/build
-pnpm-lock.yaml
-/tree-sitter-main
diff --git a/js-usfm-parser/pnpm-lock.yaml b/js-usfm-parser/pnpm-lock.yaml
deleted file mode 100644
index 56795076..00000000
--- a/js-usfm-parser/pnpm-lock.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
-  autoInstallPeers: true
-  excludeLinksFromLockfile: false
-
-importers:
-
-  .:
-    devDependencies:
-      prettier:
-        specifier: ^3.2.5
-        version: 3.2.5
-
-packages:
-
-  prettier@3.2.5:
-    resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
-    engines: {node: '>=14'}
-    hasBin: true
-
-snapshots:
-
-  prettier@3.2.5: {}
diff --git a/js-usfm-parser/prettierrc.json b/js-usfm-parser/prettierrc.json
deleted file mode 100644
index d63db584..00000000
--- a/js-usfm-parser/prettierrc.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "printWidth": 80,
-  "tabWidth": 2,
-  "semi": true,
-  "singleQuote": true,
-  "trailingComma": "none",
-  "bracketSpacing": true,
-  "jsxBracketSameLine": false,
-  "arrowParens": "always",
-  "useTabs": false
-}
diff --git a/js-usfm-parser/LICENSE b/node-usfm-parser/LICENSE
similarity index 100%
rename from js-usfm-parser/LICENSE
rename to node-usfm-parser/LICENSE
diff --git a/js-usfm-parser/README.md b/node-usfm-parser/README.md
similarity index 65%
rename from js-usfm-parser/README.md
rename to node-usfm-parser/README.md
index fd6eb9cc..feb581d9 100644
--- a/js-usfm-parser/README.md
+++ b/node-usfm-parser/README.md
@@ -14,29 +14,26 @@ npm install usfm-grammar
 Here's how you can use USFM Grammar in your JavaScript/TypeScript projects:
 
 ```javascript
-import pkg from 'usfm-grammar';
-const USFMParser = pkg.USFMParser;
-
-(async () => {
-  await USFMParser.init();
-  const usfmParser = new USFMParser()
-  const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 more text')
-  console.log({ output })
-  const usfm = usfmParser.usjToUsfm(output)
-  console.log({ usfm })
-})();
+const { USFMParser } = require('usfm-grammar');
+
+const usfmParser = new USFMParser();
+const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 some more text')
+console.log({ output });
+const usfm = usfmParser.usjToUsfm(output);
+console.log({ usfm });
+
 ```
 
-If you are using node the import part can be change as below:
+When using in an ESModule, if `import {USFMParser} from 'usfm-grammar` doesnt work for you, you could try:
 ```javascript
-const { USFMParser}  = require('usfm-grammar');
-```
+import pkg from 'usfm-grammar';
+const {USFMParser} = pkg;
 
+...
+```
 
 ## API Documentation
 
-### `USFMParser.init()`
-Initializes the USFMParser. This function must be called before creating instances of `USFMParser`. And can take the grammar and the tree-sitter  files (in wasm format) as arguments, that is included in the package.
 
 ### `USFMParser.usfmToUsj(usfmString: string): Object`
 Converts a USFM string to a USJ object.
diff --git a/node-usfm-parser/package.json b/node-usfm-parser/package.json
new file mode 100644
index 00000000..7080ae59
--- /dev/null
+++ b/node-usfm-parser/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "usfm-grammar",
+  "version": "3.0.0-alpha.8",
+  "description": "Parser using tree-sitter-usfm3, to convert usfm to usj format.",
+  "main": "./dist/cjs/index.cjs",
+  "module": "./dist/es/index.mjs",
+  "scripts": {
+    "build": "parcel build ./src/index.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/Bridgeconn/usfm-grammar/js-usfm-parser"
+  },
+  "keywords": [
+    "USFM",
+    "tree-sitter",
+    "USJ",
+    "Parser"
+  ],
+  "license": "MIT",
+  "author": "BCS Team",
+  "contributors": [
+    "Kavitha Raju <kavitha.raju@bridgeconn.com> (https://github.com/kavitharaju)",
+    "Joel Mathew <joel@bridgeconn.com> (https://github.com/joelthe1)",
+    "Samuel JD <samuelj.ernest@gmail.com> (https://github.com/samueljd)"
+  ],
+  "dependencies": {
+    "tree-sitter": "0.21.1",
+    "tree-sitter-usfm3": "^3.0.0-beta.10"
+  },
+  "devDependencies": {
+    "parcel": "^2.12.0"
+  }
+}
diff --git a/node-usfm-parser/src/filters.js b/node-usfm-parser/src/filters.js
new file mode 100644
index 00000000..08b20dd7
--- /dev/null
+++ b/node-usfm-parser/src/filters.js
@@ -0,0 +1,115 @@
+const MARKERS_WITH_DISCARDABLE_CONTENTS = [
+  "ide", "usfm", "h", "toc", "toca", "imt", "is", "ip", "ipi", "im", "imi",
+  "ipq", "imq", "ipr", "iq", "ib", "ili", "iot", "io", "iex", "imte", "ie",
+  "mt", "mte", "cl", "cd", "ms", "mr", "s", "sr", "r", "d", "sp", "sd",
+  "sts", "rem", "lit", "restore", "f", "fe", "ef", "efe", "x", "ex",
+  "fr", "ft", "fk", "fq", "fqa", "fl", "fw", "fp", "fv", "fdc",
+  "xo", "xop", "xt", "xta", "xk", "xq", "xot", "xnt", "xdc",
+  "jmp", "fig", "cat", "esb", "b"
+];
+
+const trailingNumPattern = /\d+$/;
+const punctPatternNoSpaceBefore = /^[,.\-—/;:!?@$%^)}\]>”»]/;
+const punctPatternNoSpaceAfter = /[\-—/`@^&({[<“«]$/;
+
+function combineConsecutiveTextContents(contentsList) {
+  let textCombinedContents = [];
+  let textContents = '';
+  contentsList.forEach(item => {
+    if (typeof item === 'string') {
+      if (!(textContents.endsWith(" ") || item.startsWith(" ") || textContents === '' ||
+        punctPatternNoSpaceBefore.test(item) || punctPatternNoSpaceAfter.test(textContents))) {
+        textContents += " ";
+      }
+      textContents += item;
+    } else {
+      if (textContents !== "") {
+        textCombinedContents.push(textContents);
+        textContents = "";
+      }
+      textCombinedContents.push(item);
+    }
+  });
+  if (textContents !== "") {
+    textCombinedContents.push(textContents);
+  }
+  return textCombinedContents;
+}
+
+function excludeMarkersInUsj(inputUsj, excludeMarkers, combineTexts = true, excludedParent = false) {
+  if (typeof inputUsj === 'string') {
+    if (excludedParent && excludeMarkers.includes('text-in-excluded-parent')) {
+      return [];
+    }
+    return [inputUsj];
+  }
+
+  let cleanedKids = [];
+  let cleanedMarkers = excludeMarkers.map(marker => marker.replace(trailingNumPattern, ''));
+  let thisMarker = 'marker' in inputUsj ? inputUsj.marker.replace(trailingNumPattern, '') : '';
+  let thisMarkerNeeded = true;
+  let innerContentNeeded = true;
+  excludedParent = false;
+
+  if (cleanedMarkers.includes(thisMarker)) {
+    thisMarkerNeeded = false;
+    excludedParent = true;
+    if (MARKERS_WITH_DISCARDABLE_CONTENTS.includes(thisMarker)) {
+      innerContentNeeded = false;
+    }
+  }
+
+  if ((thisMarkerNeeded || innerContentNeeded) && "content" in inputUsj) {
+    inputUsj.content.forEach(item => {
+      let cleaned = excludeMarkersInUsj(item, excludeMarkers, combineTexts, excludedParent);
+      if (Array.isArray(cleaned)) {
+        cleanedKids.push(...cleaned);
+      } else {
+        cleanedKids.push(cleaned);
+      }
+    });
+    if (combineTexts) {
+      cleanedKids = combineConsecutiveTextContents(cleanedKids);
+    }
+  }
+
+  if (thisMarkerNeeded) {
+    let cleanedUsj = { ...inputUsj, content: cleanedKids };
+    return cleanedUsj;
+  }
+  return innerContentNeeded ? cleanedKids : [];
+}
+
+function includeMarkersInUsj(inputUsj, includeMarkers, combineTexts = true, excludedParent = false) {
+  if (typeof inputUsj === 'string') {
+    return excludedParent ? [] : [inputUsj];
+  }
+  let cleanedKids = [];
+  let cleanedMarkers = includeMarkers.map(marker => marker.replace(trailingNumPattern, ''));
+  let thisMarker = 'marker' in inputUsj ? inputUsj.marker.replace(trailingNumPattern, '') : '';
+  let thisMarkerNeeded = cleanedMarkers.includes(thisMarker) || thisMarker === '';
+  let innerContentNeeded = thisMarkerNeeded || MARKERS_WITH_DISCARDABLE_CONTENTS.includes(thisMarker);
+
+  if (innerContentNeeded && "content" in inputUsj) {
+    inputUsj.content.forEach(item => {
+      let cleaned = includeMarkersInUsj(item, includeMarkers, combineTexts, !thisMarkerNeeded);
+      if (Array.isArray(cleaned)) {
+        cleanedKids.push(...cleaned);
+      } else {
+        cleanedKids.push(cleaned);
+      }
+    });
+    if (combineTexts) {
+      cleanedKids = combineConsecutiveTextContents(cleanedKids);
+    }
+  }
+
+  if (thisMarkerNeeded) {
+    let cleanedUsj = { ...inputUsj, content: cleanedKids };
+    return cleanedUsj;
+  }
+  return innerContentNeeded ? cleanedKids : [];
+}
+
+exports.excludeMarkersInUsj = excludeMarkersInUsj;
+exports.includeMarkersInUsj = includeMarkersInUsj;
diff --git a/node-usfm-parser/src/index.js b/node-usfm-parser/src/index.js
new file mode 100644
index 00000000..8d0b7af0
--- /dev/null
+++ b/node-usfm-parser/src/index.js
@@ -0,0 +1,2 @@
+const {USFMParser} = require("./usfmParser");
+exports.USFMParser = USFMParser;
\ No newline at end of file
diff --git a/node-usfm-parser/src/usfmGenerator.js b/node-usfm-parser/src/usfmGenerator.js
new file mode 100644
index 00000000..e8ad3242
--- /dev/null
+++ b/node-usfm-parser/src/usfmGenerator.js
@@ -0,0 +1,62 @@
+const { NO_USFM_USJ_TYPES, CLOSING_USJ_TYPES, NON_ATTRIB_USJ_KEYS, NO_NEWLINE_USJ_TYPES } = require("./utils/types");
+class USFMGenerator {
+  constructor() {
+    this.usfmString = "";
+  }
+
+  usjToUsfm(usjObj, nested = false) {
+
+    if (!NO_USFM_USJ_TYPES.includes(usjObj.type)) {
+      this.usfmString += "\\";
+      if (nested && usjObj.type === "char") {
+        this.usfmString += "+";
+      }
+      this.usfmString += `${usjObj.marker} `;
+    }
+    ["code", "number", "caller"].forEach((key) => {
+      if (usjObj[key]) {
+        this.usfmString += `${usjObj[key]} `;
+      }
+    });
+    if (usjObj.category) {
+      this.usfmString += `\\cat ${usjObj.category}\\cat*\n`;
+    }
+    if (Array.isArray(usjObj.content)) {
+      usjObj.content.forEach((item) => {
+        if (typeof item === "string") {
+          this.usfmString += item;
+        } else {
+          this.usjToUsfm(item, usjObj.type === "char");
+        }
+      });
+    }
+
+    let attributes = [];
+    Object.keys(usjObj).forEach((key) => {
+      if (!NON_ATTRIB_USJ_KEYS.includes(key)) {
+        attributes.push(`${key}="${usjObj[key]}"`);
+      }
+    });
+
+    if (attributes.length > 0) {
+      this.usfmString += `|${attributes.join(" ")}`;
+    }
+
+    if (CLOSING_USJ_TYPES.includes(usjObj.type)) {
+      this.usfmString += `\\`;
+      if (nested && usjObj.type === "char") {
+        this.usfmString += "+";
+      }
+      this.usfmString += `${usjObj.marker}* `;
+    }
+    if (
+      !NO_NEWLINE_USJ_TYPES.includes(usjObj.type) &&
+      this.usfmString[this.usfmString.length - 1] !== "\n"
+    ) {
+      this.usfmString += "\n";
+    }
+    return this.usfmString;
+  }
+}
+
+exports.USFMGenerator = USFMGenerator;
diff --git a/node-usfm-parser/src/usfmParser.js b/node-usfm-parser/src/usfmParser.js
new file mode 100644
index 00000000..135cbfea
--- /dev/null
+++ b/node-usfm-parser/src/usfmParser.js
@@ -0,0 +1,124 @@
+const Parser = require('tree-sitter');
+
+const {USFMGenerator} = require("./usfmGenerator");
+const {USJGenerator} = require("./usjGenerator"); 
+const { includeMarkersInUsj, excludeMarkersInUsj } = require("./filters.js");
+const USFM3 = require('tree-sitter-usfm3');
+const { Query } = Parser;
+
+class USFMParser {
+
+	constructor() {
+		this.parser = null;
+		this.usfm = null;
+		this.usj = null;
+		this.syntaxTree = null;
+		this.initializeParser()
+	}
+	initializeParser() {
+		this.parser = new Parser();
+		this.parser.setLanguage(USFM3);
+	}
+
+	usfmToUsj(usfmString) {
+		if (typeof usfmString !== "string" || usfmString === null) {
+			throw new Error("Invalid input for USFM. Expected a string.");
+		}
+		if (!this.parser) {
+			this.initializeParser();
+		}
+		this.usfm = usfmString;
+		this.parseUSFM();
+		this.usj = this.convertUSFMToUSJ();
+		return this.usj;
+	}
+
+	usjToUsfm(usjObject) {
+		if (typeof usjObject !== "object" || usjObject === null) {
+			throw new Error("Invalid input for USJ. Expected an object.");
+		}
+		if (!this.parser) {
+			this.initializeParser();
+		}
+		this.usj = usjObject;
+		this.usfm = this.convertUSJToUSFM();
+		return this.usfm;
+	}
+
+	parseUSFM() {
+		let tree = null;
+		try {
+			tree = this.parser.parse(this.usfm);
+		} catch (err) {
+			console.log(err.toString());
+		}
+		const error = this.checkForErrors(tree);
+		if (error) throw error;
+		this.syntaxTree = tree.rootNode;
+	}
+
+
+	checkForErrors(tree) {
+		const errorQuery = new Query(USFM3, "(ERROR) @errors");
+		const errors = errorQuery.captures(tree.rootNode);
+		if (errors.length > 0) {
+			this.errors = errors.map(
+				(err) =>
+					`At ${err.node.startPosition.row}:${err.node.startPosition.column}, Error: ${this.usfm.substring(err.node.startIndex, err.node.endIndex)}`,
+			);
+			return new Error(`Errors found in USFM: ${this.errors.join(", ")}`);
+		}
+		return null;
+	}
+
+	convertUSJToUSFM() {
+		const outputUSFM = new USFMGenerator().usjToUsfm(this.usj); // Simulated conversion
+		return outputUSFM;
+	}
+
+	convertUSFMToUSJ({
+		excludeMarkers = null,
+		includeMarkers = null,
+		ignoreErrors = false,
+		combineTexts = true,
+	} = {}) {
+		if (!ignoreErrors && this.errors) {
+			let errorString = this.errors.map((err) => err.join(":")).join("\n\t");
+			throw new Error(
+				`Errors present:\n\t${errorString}\nUse ignoreErrors = true to generate output despite errors.`,
+			);
+		}
+
+		let outputUSJ;
+		try {
+			let usjGenerator = new USJGenerator(
+				USFM3,
+				this.usfm
+			);
+
+			usjGenerator.nodeToUSJ(this.syntaxTree, usjGenerator.jsonRootObj);
+			outputUSJ = usjGenerator.jsonRootObj;
+		} catch (err) {
+			let message = "Unable to do the conversion. ";
+			if (this.errors) {
+				let errorString = this.errors.map((err) => err.join(":")).join("\n\t");
+				message += `Could be due to an error in the USFM\n\t${errorString}`;
+			}
+			else {
+				message = err.message;
+			}
+			return {error: message};
+		}
+
+		if (includeMarkers) {
+			outputUSJ = includeMarkersInUsj(outputUSJ, [...includeMarkers, 'USJ'], combineTexts);
+		}
+		if (excludeMarkers) {
+			outputUSJ = excludeMarkersInUsj(outputUSJ, excludeMarkers, combineTexts);
+		}
+
+		return outputUSJ;
+	}
+}
+
+exports.USFMParser = USFMParser;
diff --git a/node-usfm-parser/src/usfmParserWeb.js b/node-usfm-parser/src/usfmParserWeb.js
new file mode 100644
index 00000000..e4e35dcf
--- /dev/null
+++ b/node-usfm-parser/src/usfmParserWeb.js
@@ -0,0 +1,143 @@
+const Parser = require('web-tree-sitter');
+
+const {USFMGenerator} = require("./usfmGenerator");
+const {USJGenerator} = require("./usjGenerator"); 
+const { includeMarkersInUsj, excludeMarkersInUsj } = require("./filters.js");
+
+let USFM3 = null;
+
+class USFMParser {
+
+	constructor() {
+		this.parser = null;
+		this.usfm = null;
+		this.usj = null;
+		this.syntaxTree = null;
+		this.language = null;
+		this.errorQuery = null;
+		this.initializeParser()
+	}
+
+	static async init(grammarPath='src/grammar/tree-sitter-usfm.wasm') {
+		await Parser.init();
+		USFM3 = await Parser.Language.load(grammarPath);
+	}
+
+	initializeParser() {
+		this.parser = new Parser();
+		this.language = USFM3;
+		this.parser.setLanguage(USFM3);
+
+
+	}
+
+	usfmToUsj(usfmString) {
+		if (typeof usfmString !== "string" || usfmString === null) {
+			throw new Error("Invalid input for USFM. Expected a string.");
+		}
+		if (!this.parser) {
+			this.init();
+		}
+		this.usfm = usfmString;
+		this.parseUSFM();
+		this.usj = this.convertUSFMToUSJ();
+		return this.usj;
+	}
+
+	usjToUsfm(usjObject) {
+		if (typeof usjObject !== "object" || usjObject === null) {
+			throw new Error("Invalid input for USJ. Expected an object.");
+		}
+		if (!this.parser) {
+			this.initializeParser();
+		}
+		this.usj = usjObject;
+		this.usfm = this.convertUSJToUSFM();
+		return this.usfm;
+	}
+
+	parseUSFM() {
+		let tree = null;
+		try {
+			tree = this.parser.parse(this.usfm);
+		} catch (err) {
+			console.log(err.toString());
+		}
+		const error = this.checkForErrors(tree);
+		if (error) throw error;
+		this.syntaxTree = tree.rootNode;
+	}
+
+
+	checkForErrors(tree) {
+		const errorQuery = this.parser.getLanguage().query("(ERROR) @errors");
+		const errors = errorQuery.captures(tree.rootNode);
+		if (errors.length > 0) {
+			this.errors = errors.map(
+				(err) =>
+					`At ${err.node.startPosition.row}:${err.node.startPosition.column}, Error: ${this.usfm.substring(err.node.startIndex, err.node.endIndex)}`,
+			);
+			return new Error(`Errors found in USFM: ${this.errors.join(", ")}`);
+		}
+		return null;
+	}
+
+	convertUSJToUSFM() {
+		const outputUSFM = new USFMGenerator().usjToUsfm(this.usj); // Simulated conversion
+		return outputUSFM;
+	}
+
+	convertUSFMToUSJ({
+		excludeMarkers = null,
+		includeMarkers = null,
+		ignoreErrors = false,
+		combineTexts = true,
+	} = {}) {
+		if (!ignoreErrors && this.errors) {
+			let errorString = this.errors.map((err) => err.join(":")).join("\n\t");
+			throw new Error(
+				`Errors present:\n\t${errorString}\nUse ignoreErrors = true to generate output despite errors.`,
+			);
+		}
+
+		let outputUSJ;
+		try {
+			let usjGenerator = new USJGenerator(
+				this.language,
+				this.usfm,
+				this.queryClass
+			);
+
+			usjGenerator.nodeToUSJ(this.syntaxTree, usjGenerator.jsonRootObj);
+			outputUSJ = usjGenerator.jsonRootObj;
+		} catch (err) {
+			let message = "Unable to do the conversion. ";
+			if (this.errors) {
+				let errorString = this.errors.map((err) => err.join(":")).join("\n\t");
+				message += `Could be due to an error in the USFM\n\t${errorString}`;
+			}
+			else {
+				message = err.message;
+			}
+			return {error: message};
+		}
+
+		if (includeMarkers) {
+			outputUSJ = includeMarkersInUsj(outputUSJ, [...includeMarkers, 'USJ'], combineTexts);
+		}
+		if (excludeMarkers) {
+			outputUSJ = excludeMarkersInUsj(outputUSJ, excludeMarkers, combineTexts);
+		}
+
+		return outputUSJ;
+	}
+}
+
+async function test() {
+	myparser = new USFMParser();
+	await myparser.init();
+	console.log(myparser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 test text'));
+}
+
+// test();
+exports.USFMParser = USFMParser;
diff --git a/node-usfm-parser/src/usjGenerator.js b/node-usfm-parser/src/usjGenerator.js
new file mode 100644
index 00000000..42a3b19f
--- /dev/null
+++ b/node-usfm-parser/src/usjGenerator.js
@@ -0,0 +1,553 @@
+//Logics for syntax-tree to dict(USJ) conversions
+const Parser = require("tree-sitter");
+const {Query} = Parser;
+
+const { PARA_STYLE_MARKERS, NOTE_MARKERS, CHAR_STYLE_MARKERS, NESTED_CHAR_STYLE_MARKERS, DEFAULT_ATTRIB_MAP, TABLE_CELL_MARKERS, MISC_MARKERS } = require("./utils/markers");
+class USJGenerator {
+
+
+  constructor(treeSitterLanguageObj, usfmString, usjRootObj=null) {
+    this.usfmLanguage = treeSitterLanguageObj;
+    this.usfm = usfmString;
+    this.jsonRootObj = usjRootObj || {
+      type: "USJ",
+      version: "0.3.0",
+      content: [],
+    };
+  }
+
+  findLastFromJson(jsonObj, typeValue) {
+    let output = null;
+    if (
+      typeValue === jsonObj.type ||
+      (jsonObj.marker && typeValue === jsonObj.marker)
+    ) {
+      output = jsonObj;
+    }
+    if (jsonObj.content) {
+      jsonObj.content.forEach((child) => {
+        if (typeof child === "string") {
+          return;
+        }
+        const childOutput = this.findLastFromJson(child, typeValue);
+        if (childOutput !== null) {
+          output = childOutput;
+        }
+      });
+    }
+    return output;
+  }
+
+  nodeToUSJId(node, parentJsonObj) {
+    const idCaptures = new Query(this.usfmLanguage, 
+      "(id (bookcode) @book-code (description)? @desc)")
+      .captures(node);
+    let code = null;
+    let desc = null;
+    idCaptures.forEach((capture) => {
+      if (capture.name === "book-code") {
+        code = this.usfm.slice(capture.node.startIndex, capture.node.endIndex);
+      } else if (capture.name === "desc") {
+        desc = this.usfm.slice(capture.node.startIndex, capture.node.endIndex);
+      }
+    });
+    const bookJsonObj = {
+      type: "book",
+      marker: "id",
+      code: code,
+      content: [],
+    };
+    if (desc && desc.trim() !== "") {
+      bookJsonObj.content.push(desc.trim());
+    }
+    parentJsonObj.content.push(bookJsonObj);
+  }
+
+  // Similar conversion methods for other node types
+  nodeToUSJC(node, parentJsonObj) {
+    // Build c, the chapter milestone node in usj
+    const chapCap = new Query(this.usfmLanguage,
+        `(c (chapterNumber) @chap-num
+                                             (ca (chapterNumber) @alt-num)?
+                                             (cp (text) @pub-num)?)`,
+      )
+      .captures(node);
+    const chapNum = this.usfm.slice(
+      chapCap[0].node.startIndex,
+      chapCap[0].node.endIndex,
+    );
+    let chapRef = null;
+    this.jsonRootObj.content.forEach((child) => {
+      if (child.type === "book") {
+        chapRef = `${child.code} ${chapNum}`;
+        return;
+      }
+    });
+
+    const chapJsonObj = {
+      type: "chapter",
+      marker: "c",
+      number: chapNum,
+      sid: chapRef,
+    };
+
+    chapCap.forEach((tuple) => {
+      if (tuple[1] === "alt-num") {
+        chapJsonObj.altnumber = this.usfm
+          .substring(tuple[0].startIndex, tuple[0].endIndex)
+          .trim();
+      }
+      if (tuple[1] === "pub-num") {
+        chapJsonObj.pubnumber = this.usfm
+          .substring(tuple[0].startIndex, tuple[0].endIndex)
+          .trim();
+      }
+    });
+
+    parentJsonObj.content.push(chapJsonObj);
+
+    node.children.forEach((child) => {
+      if (["cl", "cd"].includes(child.type)) {
+        this.nodeToUSJ(child, parentJsonObj);
+      }
+    });
+  }
+
+  nodeToUSJChapter(node, parentJsonObj) {
+    // Build chapter node in USJ
+    node.children.forEach((child) => {
+      if (child.type === "c") {
+        this.nodeToUSJC(child, parentJsonObj);
+      } else {
+        this.nodeToUSJ(child, parentJsonObj);
+      }
+    });
+  }
+
+  nodeToUSJVerse(node, parentJsonObj) {
+    // Build verse node in USJ
+    const verseNumCap = new Query(this.usfmLanguage,
+        `
+      (v
+          (verseNumber) @vnum
+          (va (verseNumber) @alt)?
+          (vp (text) @vp)?
+      )`,
+      )
+      .captures(node);
+
+    const verseNum = this.usfm.substring(
+      verseNumCap[0].node.startIndex,
+      verseNumCap[0].node.endIndex,
+    );
+
+    const vJsonObj = {
+      type: "verse",
+      marker: "v",
+      number: verseNum.trim(),
+    };
+
+    verseNumCap.forEach((capture) => {
+      if (capture.name === "alt") {
+        const altNum = this.usfm.slice(
+          capture.node.startIndex,
+          capture.node.endIndex,
+        );
+        vJsonObj.altnumber = altNum;
+      } else if (capture.name === "vp") {
+        const vpText = this.usfm.substring(
+          capture.node.startIndex,
+          capture.node.endIndex,
+        );
+        vJsonObj.pubnumber = vpText;
+      }
+    });
+
+    const ref = `${this.findLastFromJson(this.jsonRootObj, "chapter").sid}:${verseNum}`;
+    vJsonObj.sid = ref.trim();
+
+    parentJsonObj.content.push(vJsonObj);
+  }
+
+  nodeToUSJCaVa(node, parentJsonObj) {
+    // Build elements for independent ca and va away from c and v
+    const style = node.type;
+    const charJsonObj = {
+      type: "char",
+      marker: style,
+    };
+
+    const altNumMatch = new Query(this.usfmLanguage,
+        `([
+        (chapterNumber)
+        (verseNumber)
+    ] @alt-num)`,
+      )
+      .captures(node);
+
+    const altNum = this.usfm
+      .slice(altNumMatch[0].node.startIndex, altNumMatch[0].node.endIndex)
+      .trim();
+
+    charJsonObj.altnumber = altNum;
+    parentJsonObj.content.push(charJsonObj);
+  }
+
+  nodeToUSJPara(node, parentJsonObj) {
+    // Build paragraph nodes in USJ
+    if (node.children[0].type.endsWith("Block")) {
+      node.children[0].children.forEach((child) => {
+        this.nodeToUSJPara(child, parentJsonObj);
+      });
+    } else if (node.type === "paragraph") {
+      const paraTagCap = new Query(this.usfmLanguage,
+        "(paragraph (_) @para-marker)")
+        .captures(node)[0];
+      const paraMarker = paraTagCap.node.type;
+
+      if (paraMarker === "b") {
+        this.nodeToUSJSpecial(paraTagCap, parentJsonObj);
+      } else if (!paraMarker.endsWith("Block")) {
+        const paraJsonObj = { type: "para", marker: paraMarker, content: [] };
+        paraTagCap.node.children.forEach((child) => {
+          this.nodeToUSJ(child, paraJsonObj);
+        });
+        parentJsonObj.content.push(paraJsonObj);
+      }
+    } else if (["pi", "ph"].includes(node.type)) {
+      const paraMarker = this.usfm
+        .substring(node.children[0].startIndex, node.children[0].endIndex)
+        .replace("\\", "")
+        .trim();
+      const paraJsonObj = { type: "para", marker: paraMarker, content: [] };
+      node.children.slice(1).forEach((child) => {
+        this.nodeToUSJ(child, paraJsonObj);
+      });
+      parentJsonObj.content.push(paraJsonObj);
+    }
+  }
+
+  nodeToUSJNotes(node, parentJsonObj) {
+    // Build USJ nodes for footnotes and cross-references
+    const tagNode = node.children[0];
+    const callerNode = node.children[1];
+    const style = this.usfm
+      .substring(tagNode.startIndex, tagNode.endIndex)
+      .replace("\\", "")
+      .trim();
+    const noteJsonObj = {
+      type: "note",
+      marker: style,
+      content: [],
+    };
+
+    noteJsonObj.caller = this.usfm
+      .substring(callerNode.startIndex, callerNode.endIndex)
+      .trim();
+
+    for (let i = 2; i < node.children.length - 1; i++) {
+      this.nodeToUSJ(node.children[i], noteJsonObj);
+    }
+
+    parentJsonObj.content.push(noteJsonObj);
+  }
+
+  nodeToUSJChar(node, parentJsonObj) {
+    // Build USJ nodes for character markups, both regular and nested
+    const tagNode = node.children[0];
+    let childrenRange = node.children.length;
+    if (node.children[node.children.length - 1].type.startsWith("\\")) {
+      childrenRange -= 1; // Exclude the last node if it starts with '\', treating it as a closing node
+    }
+    const style = this.usfm
+      .substring(tagNode.startIndex, tagNode.endIndex)
+      .replace("\\", "")
+      .replace("+", "")
+      .trim();
+    const charJsonObj = {
+      type: "char",
+      marker: style,
+      content: [],
+    };
+
+    // Assume a flag for closed markup, toggle this if your conditions and data structure require
+    // charJsonObj.closed = node.children[node.children.length - 1].type.startsWith('\\');
+
+    for (let i = 1; i < childrenRange; i++) {
+      this.nodeToUSJ(node.children[i], charJsonObj);
+    }
+
+    parentJsonObj.content.push(charJsonObj);
+  }
+
+  nodeToUSJTable(node, parentJsonObj) {
+    // Handle table related components and convert to USJ
+    if (node.type === "table") {
+      const tableJsonObj = { type: "table", content: [] };
+      node.children.forEach((child) => {
+        this.nodeToUSJ(child, tableJsonObj);
+      });
+      parentJsonObj.content.push(tableJsonObj);
+    } else if (node.type === "tr") {
+      const rowJsonObj = { type: "table:row", marker: "tr", content: [] };
+      node.children.slice(1).forEach((child) => {
+        this.nodeToUSJ(child, rowJsonObj);
+      });
+      parentJsonObj.content.push(rowJsonObj);
+    } else if (TABLE_CELL_MARKERS.includes(node.type)) {
+      const tagNode = node.children[0];
+      const style = this.usfm
+        .substring(tagNode.startIndex, tagNode.endIndex)
+        .replace("\\", "")
+        .trim();
+      const cellJsonObj = {
+        type: "table:cell",
+        marker: style,
+        content: [],
+        align: style.includes("r") ? "end" : "start",
+      };
+      node.children.slice(1).forEach((child) => {
+        this.nodeToUSJ(child, cellJsonObj);
+      });
+      parentJsonObj.content.push(cellJsonObj);
+    }
+  }
+
+  nodeToUSJAttrib(node, parentJsonObj) {
+    // Add attribute values to USJ elements
+    const attribNameNode = node.children[0];
+    let attribName = this.usfm
+      .slice(attribNameNode.startIndex, attribNameNode.endIndex)
+      .trim();
+
+    // Handling special cases for attribute names
+    if (attribName === "|") {
+      attribName = DEFAULT_ATTRIB_MAP[node.parent.type];
+    }
+    if (attribName === "src") {
+      // for \fig
+      attribName = "file";
+    }
+
+    const attribValCap = new Query(this.usfmLanguage,
+      "((attributeValue) @attrib-val)")
+      .captures(node);
+
+    let attribValue = "";
+    if (attribValCap.length > 0) {
+      attribValue = this.usfm
+        .substring(
+          attribValCap[0].node.startIndex,
+          attribValCap[0].node.endIndex,
+        )
+        .trim();
+    }
+
+    parentJsonObj[attribName] = attribValue;
+  }
+
+  nodeToUSJMilestone(node, parentJsonObj) {
+    // Create ms node in USJ
+
+    const msNameCap = new Query(this.usfmLanguage,
+        `(
+        [(milestoneTag)
+         (milestoneStartTag)
+         (milestoneEndTag)
+         (zSpaceTag)
+         ] @ms-name)`,
+      )
+      .captures(node)[0];
+
+    const style = this.usfm
+      .slice(msNameCap.node.startIndex, msNameCap.node.endIndex)
+      .replace("\\", "")
+      .trim();
+    const msJsonObj = { type: "ms", marker: style, content: [] };
+
+    node.children.forEach((child) => {
+      if (child.type.endsWith("Attribute")) {
+        this.nodeToUSJ(child, msJsonObj);
+      }
+    });
+
+    // Though normally milestones don't have contents, custom z-namespaces could have them
+    if (!msJsonObj.content.length) {
+      delete msJsonObj.content; // Remove empty content array if not used
+    }
+
+    parentJsonObj.content.push(msJsonObj);
+  }
+
+  nodeToUSJSpecial(node, parentJsonObj) {
+    // Build nodes for esb, cat, fig, optbreak in USJ
+
+    if (node.type === "esb") {
+      const sidebarJsonObj = { type: "sidebar", marker: "esb", content: [] };
+      node.children.slice(1, -1).forEach((child) => {
+        this.nodeToUSJ(child, sidebarJsonObj);
+      });
+      parentJsonObj.content.push(sidebarJsonObj);
+    } else if (node.type === "cat") {
+      const catCap = new Query(this.usfmLanguage,
+        "((category) @category)")
+        .captures(node)[0];
+      const category = this.usfm
+        .substring(catCap[0].startIndex, catCap[0].endIndex)
+        .trim();
+      parentJsonObj.category = category;
+    } else if (node.type === "fig") {
+      const figJsonObj = { type: "figure", marker: "fig", content: [] };
+      node.children.slice(1, -1).forEach((child) => {
+        this.nodeToUSJ(child, figJsonObj);
+      });
+      parentJsonObj.content.push(figJsonObj);
+    } else if (node.type === "b") {
+      const bJsonObj = { type: "optbreak", marker: "b" };
+      parentJsonObj.content.push(bJsonObj);
+    } else if (node.type === "usfm") {
+      const verJsonObj = { type: "para", marker: "usfm", content: [] };
+      const version = this.usfm
+        .substring(node.startIndex, node.endIndex)
+        .replace("\\usfm", "")
+        .trim();
+      verJsonObj.content.push(version);
+      parentJsonObj.content.push(verJsonObj);
+    }
+  }
+  nodeToUSJGeneric(node, parentJsonObj) {
+    // Build nodes for para style markers in USJ
+    const tagNode = node.children[0];
+
+    let style = this.usfm.substring(tagNode.startIndex, tagNode.endIndex);
+    if (style.startsWith("\\")) {
+      style = style.replace("\\", "").trim();
+    } else {
+      style = node.type;
+    }
+
+    // console.log(node.children.length, node.children[0].type, node.children[1].type)
+    let childrenRangeStart = 1;
+    if (
+      node.children.length > 1 &&
+      node.children[1].type.startsWith("numbered")
+    ) {
+      const numNode = node.children[1];
+      const num = this.usfm.substring(numNode.startIndex, numNode.endIndex);
+      style += num;
+      childrenRangeStart = 2;
+    }
+    const paraJsonObj = { type: "para", marker: style, content: [] };
+    parentJsonObj.content.push(paraJsonObj);
+
+    for (let i = childrenRangeStart; i < node.children.length; i++) {
+      const child = node.children[i];
+      if (
+        CHAR_STYLE_MARKERS.includes(child.type) ||
+        NESTED_CHAR_STYLE_MARKERS.includes(child.type) ||
+        [
+          "text",
+          "footnote",
+          "crossref",
+          "verseText",
+          "v",
+          "b",
+          "milestone",
+          "zNameSpace",
+        ].includes(child.type)
+      ) {
+        // Only nest these types inside the upper para style node
+        this.nodeToUSJ(child, paraJsonObj);
+      } else {
+        this.nodeToUSJ(child, parentJsonObj);
+      }
+    }
+  }
+
+  nodeToUSJ(node, parentJsonObj) {
+    // Check each node and based on the type convert to corresponding XML element
+    switch (node.type) {
+      case "id":
+        this.nodeToUSJId(node, parentJsonObj);
+        break;
+      case "chapter":
+        this.nodeToUSJChapter(node, parentJsonObj);
+        break;
+      case "cl":
+      case "cp":
+      case "cd":
+      case "vp":
+        this.nodeToUSJGeneric(node, parentJsonObj);
+        break;
+      case "ca":
+      case "va":
+        this.nodeToUSJCaVa(node, parentJsonObj);
+        break;
+      case "v":
+        this.nodeToUSJVerse(node, parentJsonObj);
+        break;
+      case "verseText":
+        node.children.forEach((child) => this.nodeToUSJ(child, parentJsonObj));
+        break;
+      case "paragraph":
+      case "pi":
+      case "ph":
+        this.nodeToUSJPara(node, parentJsonObj);
+        break;
+      case "text":
+        const textVal = this.usfm
+          .substring(node.startIndex, node.endIndex)
+          .trim();
+        if (textVal !== "") {
+          parentJsonObj.content.push(textVal);
+        }
+        break;
+      case "table":
+      case "tr":
+        this.nodeToUSJTable(node, parentJsonObj);
+        break;
+      case "milestone":
+      case "zNameSpace":
+        this.nodeToUSJMilestone(node, parentJsonObj);
+        break;
+      case "esb":
+      case "cat":
+      case "fig":
+      case "usfm":
+        this.nodeToUSJSpecial(node, parentJsonObj);
+        break;
+      default:
+        if (
+          CHAR_STYLE_MARKERS.includes(node.type) ||
+          NESTED_CHAR_STYLE_MARKERS.includes(node.type) ||
+          ["xt_standalone"].includes(node.type)
+        ) {
+          this.nodeToUSJChar(node, parentJsonObj);
+        } else if (node.type.endsWith("Attribute")) {
+          this.nodeToUSJAttrib(node, parentJsonObj);
+        } else if (
+          PARA_STYLE_MARKERS.includes(node.type) ||
+          PARA_STYLE_MARKERS.includes(
+            node.type.replace("\\", "").trim(),
+          )
+        ) {
+          this.nodeToUSJGeneric(node, parentJsonObj);
+        } else if (["", "|"].includes(node.type.trim())) {
+          // Skip white space nodes
+          break;
+        } else if (node.children.length > 0) {
+          node.children.forEach((child) =>
+            this.nodeToUSJ(child, parentJsonObj),
+          );
+        }
+        //  else {
+        //
+        //   console.error("Encountered unknown element ", node.type);
+
+        // }
+        break;
+    }
+  }
+}
+
+exports.USJGenerator = USJGenerator;
diff --git a/node-usfm-parser/src/utils/format.js b/node-usfm-parser/src/utils/format.js
new file mode 100644
index 00000000..3ed2cea0
--- /dev/null
+++ b/node-usfm-parser/src/utils/format.js
@@ -0,0 +1,12 @@
+// src/Format.js
+
+const Format = {
+  JSON: "usj",
+  CSV: "table",
+  ST: "syntax-tree",
+  USX: "usx",
+  MD: "markdown",
+  USFM: "usfm",
+};
+
+exports.Format = Format;
diff --git a/node-usfm-parser/src/utils/markers.js b/node-usfm-parser/src/utils/markers.js
new file mode 100644
index 00000000..f99c403d
--- /dev/null
+++ b/node-usfm-parser/src/utils/markers.js
@@ -0,0 +1,122 @@
+const CHAR_STYLE_MARKERS = [
+  "add",
+  "bk",
+  "dc",
+  "ior",
+  "iqt",
+  "k",
+  "litl",
+  "nd",
+  "ord",
+  "pn",
+  "png",
+  "qac",
+  "qs",
+  "qt",
+  "rq",
+  "sig",
+  "sls",
+  "tl",
+  "wj", // Special - text
+  "em",
+  "bd",
+  "bdit",
+  "it",
+  "no",
+  "sc",
+  "sup", // character styling
+  "rb",
+  "pro",
+  "w",
+  "wh",
+  "wa",
+  "wg", //special - features
+  "lik",
+  "liv", //structred list entries
+  "jmp",
+  "fr",
+  "ft",
+  "fk",
+  "fq",
+  "fqa",
+  "fl",
+  "fw",
+  "fp",
+  "fv",
+  "fdc", //footnote - content
+  "xo",
+  "xop",
+  "xt",
+  "xta",
+  "xk",
+  "xq",
+  "xot",
+  "xnt",
+  "xdc", //crossref - content
+];
+
+exports.PARA_STYLE_MARKERS = [
+  "ide",
+  "usfm",
+  "h",
+  "toc",
+  "toca", //identification
+  "imt",
+  "is",
+  "ip",
+  "ipi",
+  "im",
+  "imi",
+  "ipq",
+  "imq",
+  "ipr",
+  "iq",
+  "ib",
+  "ili",
+  "iot",
+  "io",
+  "iex",
+  "imte",
+  "ie", // intro
+  "mt",
+  "mte",
+  "cl",
+  "cd",
+  "ms",
+  "mr",
+  "s",
+  "sr",
+  "r",
+  "d",
+  "sp",
+  "sd", //titles
+  "q",
+  "qr",
+  "qc",
+  "qa",
+  "qm",
+  "qd", //poetry
+  "lh",
+  "li",
+  "lf",
+  "lim",
+  "litl", //lists
+  "sts",
+  "rem",
+  "lit",
+  "restore", //comments
+];
+exports.NOTE_MARKERS = ["f", "fe", "ef", "efe", "x", "ex"];
+exports.CHAR_STYLE_MARKERS = CHAR_STYLE_MARKERS;
+exports.NESTED_CHAR_STYLE_MARKERS = CHAR_STYLE_MARKERS.map(
+  (item) => item + "Nested",
+);
+exports.DEFAULT_ATTRIB_MAP = {
+  w: "lemma",
+  rb: "gloss",
+  xt: "link-href",
+  fig: "alt",
+  xt_standalone: "link-href",
+};
+exports.TABLE_CELL_MARKERS = ["tc", "th", "tcr", "thr"];
+exports.MISC_MARKERS = ["fig", "cat", "esb", "b", "ph", "pi"];
diff --git a/node-usfm-parser/src/utils/types.js b/node-usfm-parser/src/utils/types.js
new file mode 100644
index 00000000..52cbb097
--- /dev/null
+++ b/node-usfm-parser/src/utils/types.js
@@ -0,0 +1,18 @@
+exports.NO_USFM_USJ_TYPES = ["USJ", "table"];
+exports.CLOSING_USJ_TYPES
+  = ["char", "note", "figure"];
+exports.NON_ATTRIB_USJ_KEYS = [
+  "type",
+  "marker",
+  "content",
+  "number",
+  "sid",
+  "code",
+  "caller",
+  "align",
+  "version",
+  "altnumber",
+  "pubnumber",
+  "category",
+];
+exports.NO_NEWLINE_USJ_TYPES = ["char", "note", "verse", "table:cell"];
diff --git a/web-usfm-parser/LICENSE b/web-usfm-parser/LICENSE
new file mode 100644
index 00000000..26313a92
--- /dev/null
+++ b/web-usfm-parser/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2021 Bridge Connectivity Solutions
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/web-usfm-parser/README.md b/web-usfm-parser/README.md
new file mode 100644
index 00000000..5a291e51
--- /dev/null
+++ b/web-usfm-parser/README.md
@@ -0,0 +1,98 @@
+# USFM Grammar
+
+## Description
+This is the web alternative to the (USFM-Grammar 3.x)[https://www.npmjs.com/package/usfm-grammar] library to be used from HTML, react etc. USFM Grammar is a JavaScript library for parsing and converting USFM (Unified Standard Format Markers) to/from USJ (Unified Standard JSON) format. This library provides functionalities to parse USFM strings into a syntax tree and convert them into a JSON-like structure (USJ), and vice versa.
+
+## Installation
+You can install USFM Grammar via npm:
+
+
+## Usage
+Here's how you can use USFM Grammar in your react projects:
+```bash
+npm install usfm-grammar-web
+```
+
+```javascript
+import React, { useState, useEffect } from 'react';
+import { USFMParser } from 'usfm-grammar-web';
+
+function App() {
+  const [result, setResult] = useState(null);
+  const [result2, setResult2] = useState(null);
+
+  useEffect(() => {
+    const initParser = async () => {
+      await USFMParser.init("https://cdn.jsdelivr.net/npm/usfm-grammar-web@3.0.0-alpha.1/tree-sitter-usfm.wasm",
+                            "https://cdn.jsdelivr.net/npm/usfm-grammar-web@3.0.0-alpha.1/tree-sitter.wasm");
+    };
+    initParser();
+  }, []);
+
+  const calculateValue = async () => {
+    const usfmParser = new USFMParser();
+    const output = await usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 more text');
+    setResult(JSON.stringify(output));
+    const usfm = usfmParser.usjToUsfm(output);
+    setResult2(usfm);
+  };
+
+  return (
+    <div className="App">
+      <div>
+        <button onClick={calculateValue}>Test</button>
+        <p>USJ: {result}</p>
+        <p>USFM: {result2}</p>
+      </div>
+    </div>
+  );
+}
+
+export default App;
+```
+
+It can be used directly in the HTML script tag too.
+
+```html
+<script type="module">
+  import { USFMParser } from 'https://cdn.jsdelivr.net/npm/usfm-grammar-web@3.0.0-alpha.2/dist/bundle.mjs';
+  console.log('Hello world');
+  (async () => {
+  await USFMParser.init("https://cdn.jsdelivr.net/npm/usfm-grammar-web@3.0.0-alpha.2/tree-sitter-usfm.wasm",
+                            "https://cdn.jsdelivr.net/npm/usfm-grammar-web@3.0.0-alpha.2/tree-sitter.wasm");
+  const usfmParser = new USFMParser()
+  const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 more text')
+  console.log({ output })
+  const usfm = usfmParser.usjToUsfm(output)
+  console.log({ usfm })
+  })();
+</script>
+```
+
+
+If using from react, please refer the instructions for it [here](../docs/react-usage.md).
+
+## API Documentation
+
+### `USFMParser.init()`
+Initializes the USFMParser. This function must be called before creating instances of `USFMParser`. And can take the grammar and the tree-sitter  files (in wasm format) as arguments, that is included in the package.
+
+### `USFMParser.usfmToUsj(usfmString: string): Object`
+Converts a USFM string to a USJ object.
+
+- `usfmString`: The input USFM string.
+
+Returns: A JSON-like object representing the USJ.
+
+### `USFMParser.usjToUsfm(usjObject: Object): string`
+Converts a USJ object to a USFM string.
+
+- `usjObject`: The input USJ object.
+
+Returns: The converted USFM string.
+
+## Contributing
+Contributions are welcome! If you find any issues or have suggestions for improvements, feel free to open an issue or create a pull request on [GitHub](https://github.com/your-username/usfm-grammar).
+
+## License
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/js-usfm-parser/package.json b/web-usfm-parser/package.json
similarity index 70%
rename from js-usfm-parser/package.json
rename to web-usfm-parser/package.json
index d8bd2547..2eb46794 100644
--- a/js-usfm-parser/package.json
+++ b/web-usfm-parser/package.json
@@ -1,11 +1,14 @@
 {
-  "name": "usfm-grammar",
-  "version": "3.0.0-alpha.4",
+  "name": "usfm-grammar-web",
+  "version": "3.0.0-alpha.2",
   "description": "Parser using tree-sitter-usfm3, to convert usfm to usj format.",
   "type": "module",
-  "main": "dist/cjs/index.cjs",
-  "module": "dist/esm/index.mjs",
-  "browser": "dist/umd/index.umd.js",
+  "module": "dist/bundle.mjs",
+  "alias": {
+    "fs": false,
+    "process": false,
+    "path": false
+  },
   "source": "src/index.js",
   "scripts": {
     "build": "parcel build src/index.js"
@@ -32,15 +35,11 @@
     "Samuel JD <samuelj.ernest@gmail.com> (https://github.com/samueljd)"
   ],
   "license": "MIT",
-  "dependencies": {
-    "web-tree-sitter": "^0.22.6"
-  },
   "devDependencies": {
     "@babel/core": "^7.25.2",
-    "@babel/plugin-transform-runtime": "^7.24.7",
-    "@babel/preset-env": "^7.25.3",
-    "@babel/preset-react": "^7.24.7",
-    "babel-loader": "^9.1.3",
-    "parcel": "latest"
+    "parcel": "latest",
+    "path-browserify": "^1.0.1",
+    "process": "^0.11.10",
+    "web-tree-sitter": "^0.22.6"
   }
 }
diff --git a/js-usfm-parser/src/filters.js b/web-usfm-parser/src/filters.js
similarity index 100%
rename from js-usfm-parser/src/filters.js
rename to web-usfm-parser/src/filters.js
diff --git a/js-usfm-parser/src/index.js b/web-usfm-parser/src/index.js
similarity index 100%
rename from js-usfm-parser/src/index.js
rename to web-usfm-parser/src/index.js
diff --git a/js-usfm-parser/src/usfmGenerator.js b/web-usfm-parser/src/usfmGenerator.js
similarity index 100%
rename from js-usfm-parser/src/usfmGenerator.js
rename to web-usfm-parser/src/usfmGenerator.js
diff --git a/js-usfm-parser/src/usfmParser.js b/web-usfm-parser/src/usfmParser.js
similarity index 93%
rename from js-usfm-parser/src/usfmParser.js
rename to web-usfm-parser/src/usfmParser.js
index 342bd197..a4a4d756 100644
--- a/js-usfm-parser/src/usfmParser.js
+++ b/web-usfm-parser/src/usfmParser.js
@@ -1,17 +1,20 @@
-import Parser from 'web-tree-sitter';
+import Parser from './web-tree-sitter/tree-sitter.js';
 
 import USFMGenerator from "./usfmGenerator.js";
 import USJGenerator from "./usjGenerator.js";
 import { includeMarkersInUsj, excludeMarkersInUsj } from "./filters.js";
-const locateFile = (scriptName, scriptDirectory) => {
-	return `node_modules/usfm-grammar/${scriptName}`;
-};
+
 
 class USFMParser {
 	static language = null;
 	static async init(grammarPath="node_modules/usfm-grammar/tree-sitter-usfm.wasm",
-					parserPath="node_modules/web-tree-sitter/tree-sitter.wasm") {
-		await Parser.init({ parserPath });
+
+					parserPath="node_modules/usfm-grammar/tree-sitter.wasm") {
+		await Parser.init( {
+				  locateFile() {
+				    return parserPath;
+				  },
+				} );
 		USFMParser.language = await Parser.Language.load(grammarPath);
 	}
 
diff --git a/js-usfm-parser/src/usjGenerator.js b/web-usfm-parser/src/usjGenerator.js
similarity index 100%
rename from js-usfm-parser/src/usjGenerator.js
rename to web-usfm-parser/src/usjGenerator.js
diff --git a/js-usfm-parser/src/utils/filter.js b/web-usfm-parser/src/utils/filter.js
similarity index 100%
rename from js-usfm-parser/src/utils/filter.js
rename to web-usfm-parser/src/utils/filter.js
diff --git a/js-usfm-parser/src/utils/format.js b/web-usfm-parser/src/utils/format.js
similarity index 100%
rename from js-usfm-parser/src/utils/format.js
rename to web-usfm-parser/src/utils/format.js
diff --git a/js-usfm-parser/src/utils/markers.js b/web-usfm-parser/src/utils/markers.js
similarity index 100%
rename from js-usfm-parser/src/utils/markers.js
rename to web-usfm-parser/src/utils/markers.js
diff --git a/js-usfm-parser/src/utils/types.js b/web-usfm-parser/src/utils/types.js
similarity index 100%
rename from js-usfm-parser/src/utils/types.js
rename to web-usfm-parser/src/utils/types.js
diff --git a/web-usfm-parser/src/web-tree-sitter/package.json b/web-usfm-parser/src/web-tree-sitter/package.json
new file mode 100644
index 00000000..a2091cee
--- /dev/null
+++ b/web-usfm-parser/src/web-tree-sitter/package.json
@@ -0,0 +1,3 @@
+{
+	"type": "commonjs"
+}
\ No newline at end of file


Test patch:
diff --git a/node-usfm-parser/test.js b/node-usfm-parser/test.js
new file mode 100644
index 00000000..1644e27d
--- /dev/null
+++ b/node-usfm-parser/test.js
@@ -0,0 +1,7 @@
+const {USFMParser} = require("./src/index");
+
+const usfmParser = new USFMParser();
+const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 some more text')
+console.log({ output });
+const usfm = usfmParser.usjToUsfm(output);
+console.log({ usfm });
diff --git a/js-usfm-parser/test.cjs b/node-usfm-parser/tests/basic.js
similarity index 50%
rename from js-usfm-parser/test.cjs
rename to node-usfm-parser/tests/basic.js
index 09f7fee4..a0d95758 100644
--- a/js-usfm-parser/test.cjs
+++ b/node-usfm-parser/tests/basic.js
@@ -1,11 +1,9 @@
-// To use in this repo temporarily remove the "type": "module" in package.json
-const USFMParser = require("../dist/cjs/index.cjs").default;
+const {USFMParser} = require("./src/index");
 
 (async () => {
-  await USFMParser.init();
   const usfmParser = new USFMParser()
-  const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 more text')
+  const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2')
   console.log({ output })
   const usfm = usfmParser.usjToUsfm(output)
   console.log({ usfm })
-})();
+})();
\ No newline at end of file
diff --git a/js-usfm-parser/test.js b/web-usfm-parser/test.js
similarity index 81%
rename from js-usfm-parser/test.js
rename to web-usfm-parser/test.js
index f571efb5..c24a4ea7 100644
--- a/js-usfm-parser/test.js
+++ b/web-usfm-parser/test.js
@@ -1,7 +1,7 @@
 import {USFMParser} from './src/index.js';
 
 (async () => {
-  await USFMParser.init("tree-sitter-usfm.wasm");
+  await USFMParser.init("tree-sitter-usfm.wasm", "tree-sitter.wasm");
   const usfmParser = new USFMParser();
   const output = usfmParser.usfmToUsj('\\id GEN\n\\c 1\n\\p\n\\v 1 In the begining..\\v 2 more text');
   console.log({ output });