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

Issue:
Generate USFM from other formats
To be able to provide round trippability with our various formats like USJ, USX and syntax tree we need to be able to generate USFM from these formats

- [x] USX to USFM
- [x] USJ to USFM
- [ ] Table/List/CSV to USFM
- [ ] Syntax tree to USFM (Not sure if this is useful/feasible)


Discussion / hints:

Fix patch:
diff --git a/docs/API guide for python usfm_grammar.ipynb b/docs/API guide for python usfm_grammar.ipynb
index e42b49cc..697b66f7 100644
--- a/docs/API guide for python usfm_grammar.ipynb
+++ b/docs/API guide for python usfm_grammar.ipynb
@@ -54,13 +54,53 @@
     "! pip install -e ./../py-usfm-parser/ # from the code base"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "538cbe2b-1c0e-4a93-b8fb-5d648b686cdb",
+   "metadata": {},
+   "source": [
+    "### Using from CLI"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0ea11f97-9aa2-4cb2-8f79-b77c7c4dabc7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "! usfm-grammar ../tests/basic/multiple-chapters/origin.usfm"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5914f945-3660-4ca9-8033-4afc503d53d4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "! usfm-grammar ../tests/basic/footnote/origin.usfm --exclude_markers notes --exclude_markers w"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dafaf6a1-38fb-4ecd-a3d4-570e45e9ba79",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "! usfm-grammar ../tests/basic/multiple-chapters/origin.usfm --out_format usx"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
    "id": "ba23e32e",
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "! usfm-grammar ../tests/basic/multiple-chapters/origin-usj.json --out_format usfm"
+   ]
   },
   {
    "cell_type": "code",
@@ -94,7 +134,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 4,
    "id": "5d06e40a",
    "metadata": {},
    "outputs": [],
@@ -104,7 +144,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 5,
    "id": "932320b5",
    "metadata": {},
    "outputs": [],
@@ -172,7 +212,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 6,
    "id": "ad92e648",
    "metadata": {},
    "outputs": [],
@@ -182,7 +222,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "id": "758fc216",
    "metadata": {},
    "outputs": [],
@@ -197,12 +237,16 @@
    "id": "c2b619c6",
    "metadata": {},
    "source": [
-    "#### Converting to other formats and extracting specific contents via filters"
+    "### Converting USJ and extracting specific contents via filters\n",
+    "- The easy to work with JSON representation of USFM data\n",
+    "- Exclude and include markers in output as needed from the input\n",
+    "    - Handy custom filters provided.\n",
+    "    - Also the flexibility of specifying any marker"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 8,
    "id": "9aa16315",
    "metadata": {},
    "outputs": [
@@ -277,14 +321,13 @@
        "    {'type': 'char:w',\n",
        "     'content': ['gracious'],\n",
        "     'lemma': 'grace',\n",
-       "     'x-myattr': 'metadata'},\n",
-       "    '']},\n",
+       "     'x-myattr': 'metadata'}]},\n",
        "  {'type': 'chapter:c', 'number': '2', 'sid': 'EXO 2'},\n",
        "  {'type': 'para:s1', 'content': ['A Prayer of Habakkuk']},\n",
        "  {'type': 'para:p',\n",
        "   'content': [{'type': 'verse:v', 'number': '1', 'sid': 'EXO 2:1'},\n",
-       "    'This is a prayer of the prophet Habakkuk:',\n",
-       "    {'type': 'optbreak:b'}]},\n",
+       "    'This is a prayer of the prophet Habakkuk:']},\n",
+       "  {'type': 'optbreak:b'},\n",
        "  {'type': 'para:q1',\n",
        "   'content': [{'type': 'verse:v', 'number': '2', 'sid': 'EXO 2:2'},\n",
        "    'O',\n",
@@ -298,16 +341,14 @@
        "   'content': [{'type': 'verse:v', 'number': '20', 'sid': 'EXO 2:20'},\n",
        "    'Adam',\n",
        "    {'type': 'note:f',\n",
-       "     'content': ['',\n",
-       "      {'type': 'char:fr', 'content': ['3.20:']},\n",
+       "     'content': [{'type': 'char:fr', 'content': ['3.20:']},\n",
        "      {'type': 'char:fk', 'content': ['Adam:']},\n",
        "      {'type': 'char:ft',\n",
        "       'content': ['This name in Hebrew means “all human beings.”']}],\n",
        "     'caller': '+'},\n",
        "    'named his wife Eve,',\n",
        "    {'type': 'note:f',\n",
-       "     'content': ['',\n",
-       "      {'type': 'char:fr', 'content': ['3.20:']},\n",
+       "     'content': [{'type': 'char:fr', 'content': ['3.20:']},\n",
        "      {'type': 'char:fk', 'content': ['Eve:']},\n",
        "      {'type': 'char:ft',\n",
        "       'content': ['This name sounds similar to the Hebrew\\nword for “living,” which is rendered in this context as “human beings.”']}],\n",
@@ -319,8 +360,7 @@
        "    'God made clothes out of animal skins for Adam and his wife,\\nand he clothed them.',\n",
        "    {'type': 'ms:qt-s', 'sid': 'qt_123', 'who': 'Pilate'},\n",
        "    '“Are you the king of the Jews?”',\n",
-       "    {'type': 'ms:qt-e', 'eid': 'qt_123'},\n",
-       "    '']},\n",
+       "    {'type': 'ms:qt-e', 'eid': 'qt_123'}]},\n",
        "  {'type': 'sidebar:esb',\n",
        "   'content': [{'type': 'para:ms', 'content': ['Fish and Fishing']},\n",
        "    {'type': 'para:p',\n",
@@ -331,12 +371,11 @@
        "     'content': ['Among early Christians, the fish was a favorite image for Jesus, because the Greek\\nword for fish (',\n",
        "      {'type': 'char:tl', 'content': ['ichthus']},\n",
        "      ') consists of the first letters of the Greek words that\\ntell who Jesus is:',\n",
-       "      {'type': 'figure:fig', 'content': ['Christian Fish Image']},\n",
-       "      '']}],\n",
+       "      {'type': 'figure:fig', 'content': ['Christian Fish Image']}]}],\n",
        "   'category': 'History'}]}"
       ]
      },
-     "execution_count": 6,
+     "execution_count": 8,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -365,7 +404,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 9,
    "id": "ba0b92d5",
    "metadata": {},
    "outputs": [
@@ -389,7 +428,7 @@
        "  {'type': 'verse:v', 'number': '12-83', 'sid': 'EXO 1:12-83', 'content': []},\n",
        "  'They presented their offerings in the following order: Day Tribe Leader 1st Judah Nahshon son of Amminadab 2nd Issachar Nethanel son of Zuar 3rd Zebulun Eliab son of Helon',\n",
        "  {'type': 'verse:v', 'number': '5', 'sid': 'EXO 1:5', 'content': []},\n",
-       "  'യാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു. gracious Lord and then a few words later gracious ',\n",
+       "  'യാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു. gracious Lord and then a few words later gracious',\n",
        "  {'type': 'chapter:c', 'number': '2', 'sid': 'EXO 2', 'content': []},\n",
        "  {'type': 'verse:v', 'number': '1', 'sid': 'EXO 2:1', 'content': []},\n",
        "  'This is a prayer of the prophet Habakkuk:',\n",
@@ -398,10 +437,10 @@
        "  {'type': 'verse:v', 'number': '20', 'sid': 'EXO 2:20', 'content': []},\n",
        "  'Adam named his wife Eve, because she\\nwas the mother of all human beings.',\n",
        "  {'type': 'verse:v', 'number': '21', 'sid': 'EXO 2:21', 'content': []},\n",
-       "  'And the Lord God made clothes out of animal skins for Adam and his wife,\\nand he clothed them. “Are you the king of the Jews?” ']}"
+       "  'And the Lord God made clothes out of animal skins for Adam and his wife,\\nand he clothed them. “Are you the king of the Jews?”']}"
       ]
      },
-     "execution_count": 7,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -460,9 +499,21 @@
     "                )"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "961860bc-0c3f-4f77-851f-9014ac5a3912",
+   "metadata": {},
+   "source": [
+    "### Converting to Table or List format\n",
+    " - With filtering similar to to_usj()\n",
+    " - Table format for better manual visual inspection of data\n",
+    " - Easily port data to a CSV (and to an excel worksheet)\n",
+    " - Using this from command-line lets to work with zero code"
+   ]
+  },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 10,
    "id": "cf59a547",
    "metadata": {
     "scrolled": true
@@ -475,96 +526,10 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": null,
    "id": "f56bc71d",
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Book\tChapter\tVerse\tText\tType\n",
-      "EXO\t\t\t02EXOGNT92.SFM, Good News Translation, June 2003\tbook:id\n",
-      "EXO\t\t\tപുറപ്പാടു്\tpara:h\n",
-      "EXO\t\t\tപുറപ്പാടു്\tpara:toc1\n",
-      "EXO\t\t\tപുറപ്പാടു്\tpara:toc2\n",
-      "EXO\t\t\tപുറപ്പാടു്\tpara:mt\n",
-      "EXO\t1\t1\tയാക്കോബിനോടുകൂടെ കുടുംബസഹിതം ഈജിപ്റ്റിൽ വന്ന\tpara:p\n",
-      "EXO\t1\t1\tയിസ്രായേൽമക്കളുടെ പേരുകൾ :\tpara:p\n",
-      "EXO\t1\t2\tരൂബേൻ, ശിമെയോൻ, ലേവി,\tpara:p\n",
-      "EXO\t1\t3\tയെഹൂദാ,\tpara:li1\n",
-      "EXO\t1\t3\tയിസ്സാഖാർ,\tpara:li1\n",
-      "EXO\t1\t3\tസെബൂലൂൻ,\tpara:li1\n",
-      "EXO\t1\t3\tബെന്യാമീൻ\tpara:li1\n",
-      "EXO\t1\t4\tദാൻ, നഫ്താലി, ഗാദ്, ആശേർ.\tpara:p\n",
-      "EXO\t1\t12-83\tThey presented their offerings in the following order:\tpara:p\n",
-      "EXO\t1\t12-83\tDay\ttable:cell:th1\n",
-      "EXO\t1\t12-83\tTribe\ttable:cell:th2\n",
-      "EXO\t1\t12-83\tLeader\ttable:cell:th3\n",
-      "EXO\t1\t12-83\t1st\ttable:cell:tcr1\n",
-      "EXO\t1\t12-83\tJudah\ttable:cell:tc2\n",
-      "EXO\t1\t12-83\tNahshon son of Amminadab\ttable:cell:tc3\n",
-      "EXO\t1\t12-83\t2nd\ttable:cell:tcr1\n",
-      "EXO\t1\t12-83\tIssachar\ttable:cell:tc2\n",
-      "EXO\t1\t12-83\tNethanel son of Zuar\ttable:cell:tc3\n",
-      "EXO\t1\t12-83\t3rd\ttable:cell:tcr1\n",
-      "EXO\t1\t12-83\tZebulun\ttable:cell:tc2\n",
-      "EXO\t1\t12-83\tEliab son of Helon\ttable:cell:tc3\n",
-      "EXO\t1\t5\tയാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു.\tpara:p\n",
-      "EXO\t1\t5\tgracious\tchar:w\n",
-      "EXO\t1\t5\tLord\tchar:nd\n",
-      "EXO\t1\t5\tand then a few words later\tpara:p\n",
-      "EXO\t1\t5\tgracious\tchar:w\n",
-      "EXO\t1\t5\t\tpara:p\n",
-      "EXO\t2\t5\tA Prayer of Habakkuk\tpara:s1\n",
-      "EXO\t2\t1\tThis is a prayer of the prophet Habakkuk:\tpara:p\n",
-      "EXO\t2\t2\tO\tpara:q1\n",
-      "EXO\t2\t2\tLord\tchar:nd\n",
-      "EXO\t2\t2\t, I have heard of what you have done,\tpara:q1\n",
-      "EXO\t2\t2\tand I am filled with awe.\tpara:q2\n",
-      "EXO\t2\t2\tNow do again in our times\tpara:q1\n",
-      "EXO\t2\t2\tthe great deeds you used to do.\tpara:q2\n",
-      "EXO\t2\t2\tBe merciful, even when you are angry.\tpara:q1\n",
-      "EXO\t2\t20\tAdam\tpara:p\n",
-      "EXO\t2\t20\t\tnote:f\n",
-      "EXO\t2\t20\t3.20:\tchar:fr\n",
-      "EXO\t2\t20\tAdam:\tchar:fk\n",
-      "EXO\t2\t20\tThis name in Hebrew means “all human beings.”\tchar:ft\n",
-      "EXO\t2\t20\tnamed his wife Eve,\tpara:p\n",
-      "EXO\t2\t20\t\tnote:f\n",
-      "EXO\t2\t20\t3.20:\tchar:fr\n",
-      "EXO\t2\t20\tEve:\tchar:fk\n",
-      "EXO\t2\t20\tThis name sounds similar to the Hebrew\n",
-      "word for “living,” which is rendered in this context as “human beings.”\tchar:ft\n",
-      "EXO\t2\t20\tbecause she\n",
-      "was the mother of all human beings.\tpara:p\n",
-      "EXO\t2\t21\tAnd the\tpara:p\n",
-      "EXO\t2\t21\tLord\tchar:nd\n",
-      "EXO\t2\t21\tGod made clothes out of animal skins for Adam and his wife,\n",
-      "and he clothed them.\tpara:p\n",
-      "EXO\t2\t21\t“Are you the king of the Jews?”\tpara:p\n",
-      "EXO\t2\t21\t\tpara:p\n",
-      "EXO\t2\t21\tFish and Fishing\tpara:ms\n",
-      "EXO\t2\t21\tIn Jesus' time, fishing took place mostly on lake Galilee, because Jewish people\n",
-      "could not use many of the harbors along the coast of the Mediterranean Sea, since these\n",
-      "harbors were often controlled by unfriendly neighbors. The most common fish in the Lake\n",
-      "of Galilee were carp and catfish.\tpara:p\n",
-      "EXO\t2\t21\tThe Law of Moses\tchar:wj\n",
-      "EXO\t2\t21\tallowed people to eat any fish with\n",
-      "fins and scales, but since catfish lack scales (as do eels and sharks) they were not to\n",
-      "be eaten. Fish were also probably brought from Tyre and Sidon,\n",
-      "where they were dried and salted.\n",
-      "...\tpara:p\n",
-      "EXO\t2\t21\tAmong early Christians, the fish was a favorite image for Jesus, because the Greek\n",
-      "word for fish (\tpara:p\n",
-      "EXO\t2\t21\tichthus\tchar:tl\n",
-      "EXO\t2\t21\t) consists of the first letters of the Greek words that\n",
-      "tell who Jesus is:\tpara:p\n",
-      "EXO\t2\t21\tChristian Fish Image\tfigure:fig\n",
-      "EXO\t2\t21\t\tpara:p\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "print(\"\\n\".join([\"\\t\".join(row) for row in table_output]))"
    ]
@@ -590,7 +555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 12,
    "id": "d7b6c4d0",
    "metadata": {},
    "outputs": [
@@ -605,13 +570,13 @@
       "EXO\t1\t3\tയെഹൂദാ, യിസ്സാഖാർ, സെബൂലൂൻ, ബെന്യാമീൻ\t\n",
       "EXO\t1\t4\tദാൻ, നഫ്താലി, ഗാദ്, ആശേർ.\t\n",
       "EXO\t1\t12-83\tThey presented their offerings in the following order: Day Tribe Leader 1st Judah Nahshon son of Amminadab 2nd Issachar Nethanel son of Zuar 3rd Zebulun Eliab son of Helon\t\n",
-      "EXO\t1\t5\tയാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു. gracious Lord and then a few words later gracious \t\n",
+      "EXO\t1\t5\tയാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു. gracious Lord and then a few words later gracious\t\n",
       "EXO\t2\t1\tThis is a prayer of the prophet Habakkuk:\t\n",
       "EXO\t2\t2\tO Lord, I have heard of what you have done, and I am filled with awe. Now do again in our times the great deeds you used to do. Be merciful, even when you are angry.\t\n",
       "EXO\t2\t20\tAdam named his wife Eve, because she\n",
       "was the mother of all human beings.\t\n",
       "EXO\t2\t21\tAnd the Lord God made clothes out of animal skins for Adam and his wife,\n",
-      "and he clothed them. “Are you the king of the Jews?” \t\n"
+      "and he clothed them. “Are you the king of the Jews?”\t\n"
      ]
     }
    ],
@@ -622,26 +587,10 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": null,
    "id": "43049bb6",
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Book\tChapter\tVerse\tText\tType\n",
-      "EXO\t\t\t02EXOGNT92.SFM, Good News Translation, June 2003\tbook:id\n",
-      "EXO\t2\t20\t3.20:\tchar:fr\n",
-      "EXO\t2\t20\tAdam:\tchar:fk\n",
-      "EXO\t2\t20\tThis name in Hebrew means “all human beings.”\tchar:ft\n",
-      "EXO\t2\t20\t3.20:\tchar:fr\n",
-      "EXO\t2\t20\tEve:\tchar:fk\n",
-      "EXO\t2\t20\tThis name sounds similar to the Hebrew\n",
-      "word for “living,” which is rendered in this context as “human beings.”\tchar:ft\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "table_output = my_parser.to_list(include_markers=Filter.NOTES)\n",
     "print(\"\\n\".join([\"\\t\".join(row) for row in table_output]))\n"
@@ -656,12 +605,12 @@
    "source": []
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "fe450f35",
+   "cell_type": "markdown",
+   "id": "accb38c9-e1d8-47bc-bdfc-48e4b626a486",
    "metadata": {},
-   "outputs": [],
-   "source": []
+   "source": [
+    "### Convert to USX"
+   ]
   },
   {
    "cell_type": "code",
@@ -689,7 +638,7 @@
    "id": "2a9162ca",
    "metadata": {},
    "source": [
-    "#### To work with the syntax tree itself"
+    "### To work with the syntax tree itself"
    ]
   },
   {
@@ -739,12 +688,14 @@
    "source": []
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "a680a0b6",
+   "cell_type": "markdown",
+   "id": "a35acb88-7300-4b84-bd2f-90b7cd90d8fb",
    "metadata": {},
-   "outputs": [],
-   "source": []
+   "source": [
+    "### USJ to USFM \n",
+    " - Round tripping\n",
+    " - Allows you to make edits on the USJ/SON and then create a USFM with the new data"
+   ]
   },
   {
    "cell_type": "code",
@@ -752,7 +703,13 @@
    "id": "0fac8a56",
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "\n",
+    "usj_obj = my_parser.to_usj()\n",
+    "\n",
+    "my_parser2 = USFMParser(from_usj=usj_obj)\n",
+    "print(my_parser2.usfm)"
+   ]
   },
   {
    "cell_type": "code",
@@ -764,11 +721,29 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 14,
    "id": "1ea6bb28",
    "metadata": {},
-   "outputs": [],
-   "source": []
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\\id EXO 02EXOGNT92.SFM, Good News Translation, June 2003\n",
+      "\\c 1\n",
+      "\\v 1 യാക്കോബിനോടുകൂടെ കുടുംബസഹിതം ഈജിപ്റ്റിൽ വന്ന യിസ്രായേൽമക്കളുടെ പേരുകൾ :\\v 2 രൂബേൻ, ശിമെയോൻ, ലേവി,\\v 3 യെഹൂദാ, യിസ്സാഖാർ, സെബൂലൂൻ, ബെന്യാമീൻ\\v 4 ദാൻ, നഫ്താലി, ഗാദ്, ആശേർ.\\v 12-83 They presented their offerings in the following order: Day Tribe Leader 1st Judah Nahshon son of Amminadab 2nd Issachar Nethanel son of Zuar 3rd Zebulun Eliab son of Helon\\v 5 യാക്കോബിന്റെ സന്താനപരമ്പരകൾ എല്ലാം കൂടി എഴുപതു പേർ ആയിരുന്നു; യോസേഫ് മുമ്പെ തന്നെ ഈജിപ്റ്റിൽ ആയിരുന്നു. gracious Lord and then a few words later gracious\\c 2\n",
+      "\\v 1 This is a prayer of the prophet Habakkuk:\\v 2 O Lord, I have heard of what you have done, and I am filled with awe. Now do again in our times the great deeds you used to do. Be merciful, even when you are angry.\\v 20 Adam named his wife Eve, because she\n",
+      "was the mother of all human beings.\\v 21 And the Lord God made clothes out of animal skins for Adam and his wife,\n",
+      "and he clothed them. “Are you the king of the Jews?”\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "usj_obj2 = my_parser2.to_usj(include_markers=Filter.BCV+Filter.TEXT)\n",
+    "my_parser3 = USFMParser(from_usj=usj_obj2)\n",
+    "print(my_parser3.usfm)"
+   ]
   },
   {
    "cell_type": "code",
diff --git a/py-usfm-parser/README.md b/py-usfm-parser/README.md
index 4cc6c865..4a1f8ed8 100644
--- a/py-usfm-parser/README.md
+++ b/py-usfm-parser/README.md
@@ -86,10 +86,44 @@ print(table_output)
 
 ```
 
+To round trip with USJ
+```
+from usfm_grammar import USFMParser, Filter
+
+my_parser = USFMParser(input_usfm_str)
+usj_obj = my_parser.to_usj()
+
+my_parser2 = USFMParser(from_usj=usj_obj)
+print(my_parser2.usfm)
+```
+:warning: There will be differences between first USFM and the generated one in 1. Spaces and lines 2. Default attributes will be given their names 3. Closing markers may be newly added
+
+To remove unwanted markers from USFM
+```
+from usfm_grammar import USFMParser, Filter, USFMGenerator
+
+my_parser = USFMParser(input_usfm_str)
+usj_obj = my_parser.to_usj(include_markers=Filter.BCV+Filter.TEXT)
+
+my_parser2 = USFMParser(from_usj=usj_obj)
+print(my_parser2.usfm)
+```
+USJ to USX or Table
+```
+rom usfm_grammar import USFMParser, Filter
+
+my_parser = USFMParser(input_usfm_str)
+usj_obj = my_parser.to_usj()
+
+my_parser2 = USFMParser(from_usj=usj_obj)
+print(my_parser2.to_usx())
+# print(my_parser2.to_list())
+```
 ### From CLI
 
 ```
-usage: usfm-grammar [-h] [--format {json,table,syntax-tree,usx,markdown}]
+usage: usfm-grammar [-h] [--in_format {usfm,usj}]
+                    [--out_format {usj,table,syntax-tree,usx,markdown,usfm}]
                     [--include_markers {book_headers,titles,...}]
                     [--exclude_markers {book_headers,titles,...}]
                     [--csv_col_sep CSV_COL_SEP] [--csv_row_sep CSV_ROW_SEP]
@@ -100,11 +134,13 @@ Uses the tree-sitter-usfm grammar to parse and convert USFM to Syntax-tree,
 JSON, CSV, USX etc.
 
 positional arguments:
-  infile                input usfm file
+  infile                input usfm or usj file
 
 options:
   -h, --help            show this help message and exit
-  --format {json,table,syntax-tree,usx,markdown}
+  --in_format {usfm,usj}
+                        input file format
+  --out_format {usj,table,syntax-tree,usx,markdown,usfm}
                         output format
   --include_markers {book_headers,titles,comments,paragraphs,characters,notes,study_bible,bcv,text,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,p,m,po,pr,cls,pmo,pm,pmc,pmr,pi,mi,nb,pc,ph,q,qr,qc,qa,qm,qd,lh,li,lf,lim,litl,tr,tc,th,tcr,thr,table,b,add,bk,dc,ior,iqt,k,litl,nd,ord,pn,png,qac,qs,qt,rq,sig,sls,tl,wj,em,bd,bdit,it,no,sc,sup,rb,pro,w,wh,wa,wg,lik,liv,jmp,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,esb,cat,id,c,v,text-in-excluded-parent}
                         the list of of contents to be included
@@ -123,11 +159,15 @@ options:
 ```
 Example
 ```
->>> python3 -m usfm_grammar sample.usfm --format usx
+>>> python3 -m usfm_grammar sample.usfm --out_format usx
 
->>> usfm-grammar sample.usfm --format usx
+>>> usfm-grammar sample.usfm
+
+>>> usfm-grammar sample.usfm --out_format usx
 
 >>> usfm-grammar sample.usfm --include_markers bcv --include_markers text --include_markers s
+
+>>> usfm-grammar sample-usj.json --out_format usfm
 ```
 
 ### Filtering on USJ
diff --git a/py-usfm-parser/src/usfm_grammar/__init__.py b/py-usfm-parser/src/usfm_grammar/__init__.py
index ac122f77..7c8a47ee 100755
--- a/py-usfm-parser/src/usfm_grammar/__init__.py
+++ b/py-usfm-parser/src/usfm_grammar/__init__.py
@@ -1,6 +1,7 @@
 '''Entry point of the package with its public values'''
 
 from usfm_grammar import usfm_parser
+from usfm_grammar import usfm_generator
 
 Filter = usfm_parser.Filter
 Format = usfm_parser.Format
diff --git a/py-usfm-parser/src/usfm_grammar/__main__.py b/py-usfm-parser/src/usfm_grammar/__main__.py
index cc0ba2b2..28807335 100755
--- a/py-usfm-parser/src/usfm_grammar/__main__.py
+++ b/py-usfm-parser/src/usfm_grammar/__main__.py
@@ -11,14 +11,60 @@
 for member in Filter:
     all_markers += member.value
 
+def handle_input_file(arg_parser):
+    '''If initialsing with USFM or USJ?'''
+    infile = arg_parser.parse_args().infile
+    input_format = arg_parser.parse_args().in_format
+    with open(infile, 'r', encoding='utf-8') as usfm_file:
+        file_content = usfm_file.read()
+
+    if input_format == Format.JSON or infile.split(".")[-1] in ['json', 'usj']:
+        usj_obj = json.loads(file_content)
+        my_parser = USFMParser(from_usj=usj_obj)
+    elif input_format == Format.USFM:
+        my_parser = USFMParser(file_content)
+    else:
+        raise Exception("Un-recognized input_format!")
+    return my_parser
+
+def handle_include_exclude_options(arg_parser):
+    '''Process list of markers and ENUMs'''
+    exclude_markers = arg_parser.parse_args().exclude_markers
+    include_markers = arg_parser.parse_args().include_markers
+
+    filter_names =  [member.name for member in Filter]
+    if exclude_markers is None:
+        updated_exclude_markers = None
+    else:
+        updated_exclude_markers = []
+        for itm in exclude_markers:
+            if itm.upper() in filter_names:
+                updated_exclude_markers += Filter[itm.upper()]
+            else:
+                updated_exclude_markers.append(itm.lower().replace("\\", ""))
+    if include_markers is None:
+        updated_include_markers = None
+    else:
+        updated_include_markers = []
+        for itm in include_markers:
+            if itm.upper() in filter_names:
+                updated_include_markers += Filter[itm.upper()]
+            else:
+                updated_include_markers.append(itm.lower().replace("\\", ""))
+    return updated_exclude_markers, updated_include_markers
+
 
 def main(): #pylint: disable=too-many-locals
     '''handles the command line requests'''
     arg_parser = argparse.ArgumentParser(
         description='Uses the tree-sitter-usfm grammar to parse and convert USFM to '+\
         'Syntax-tree, JSON, CSV, USX etc.')
-    arg_parser.add_argument('infile', type=str, help='input usfm file')
-    arg_parser.add_argument('--format', type=str, help='output format',
+    arg_parser.add_argument('infile', type=str, help='input usfm or usj file')
+
+    arg_parser.add_argument('--in_format', type=str, help='input file format',
+                            choices=[Format.USFM.value, Format.JSON.value],
+                            default=Format.USFM.value)
+    arg_parser.add_argument('--out_format', type=str, help='output format',
                             choices=[itm.value for itm in Format],
                             default=Format.JSON.value)
     arg_parser.add_argument('--include_markers', type=str,
@@ -44,52 +90,29 @@ def main(): #pylint: disable=too-many-locals
                             'from different components, or not',
                             action='store_true')
 
-    infile = arg_parser.parse_args().infile
-    output_format = arg_parser.parse_args().format
-    exclude_markers = arg_parser.parse_args().exclude_markers
-    include_markers = arg_parser.parse_args().include_markers
-
-    with open(infile, 'r', encoding='utf-8') as usfm_file:
-        file_content = usfm_file.read()
 
-    my_parser = USFMParser(file_content)
+    my_parser = handle_input_file(arg_parser)
 
     if my_parser.errors and not arg_parser.parse_args().ignore_errors:
         err_str = "\n\t".join([":".join(err) for err in my_parser.errors])
         print(f"Errors present:\n\t{err_str}")
         sys.exit(1)
 
-    filter_names =  [member.name for member in Filter]
-    if exclude_markers is None:
-        updated_exclude_markers = None
-    else:
-        updated_exclude_markers = []
-        for itm in exclude_markers:
-            if itm.upper() in filter_names:
-                updated_exclude_markers += Filter[itm.upper()]
-            else:
-                updated_exclude_markers.append(itm.lower().replace("\\", ""))
-    if include_markers is None:
-        updated_include_markers = None
-    else:
-        updated_include_markers = []
-        for itm in include_markers:
-            if itm.upper() in filter_names:
-                updated_include_markers += Filter[itm.upper()]
-            else:
-                updated_include_markers.append(itm.lower().replace("\\", ""))
+    exclude_markers, include_markers = handle_include_exclude_options(arg_parser)
+
+    output_format = arg_parser.parse_args().out_format
 
     match output_format:
         case Format.JSON:
             dict_output = my_parser.to_usj(
-                exclude_markers=updated_exclude_markers,
-                include_markers=updated_include_markers,
+                exclude_markers=exclude_markers,
+                include_markers=include_markers,
                 ignore_errors=True)
             print(json.dumps(dict_output, indent=4, ensure_ascii=False))
         case Format.CSV:
             table_output = my_parser.to_list(
-                exclude_markers=updated_exclude_markers,
-                include_markers=updated_include_markers,
+                exclude_markers=exclude_markers,
+                include_markers=include_markers,
                 ignore_errors=True)
             outfile = sys.stdout
             writer = csv.writer(outfile,
@@ -104,6 +127,8 @@ def main(): #pylint: disable=too-many-locals
             print(my_parser.to_markdown())
         case Format.ST:
             print(my_parser.to_syntax_tree(ignore_errors=True))
+        case Format.USFM:
+            print(my_parser.usfm)
         case _:
             raise Exception(f"Un-recognized output format:{output_format}!")
 
diff --git a/py-usfm-parser/src/usfm_grammar/usfm_generator.py b/py-usfm-parser/src/usfm_grammar/usfm_generator.py
new file mode 100644
index 00000000..9d726c20
--- /dev/null
+++ b/py-usfm-parser/src/usfm_grammar/usfm_generator.py
@@ -0,0 +1,89 @@
+'''Convert other formats back into USFM'''
+
+NO_USFM_USJ_TYPES = ['USJ', 'table']
+NO_NEWLINE_USJ_TYPES = ['char', 'note', 'verse', 'table:cell']
+CLOSING_USJ_TYPES = ['char', 'note', 'figure']
+NON_ATTRIB_USJ_KEYS = ['type', 'content', 'number', 'sid',
+                        'code', 'caller', 'align',
+                        'version', 'altnumber', 'pubnumber', 'category']
+
+class USFMGenerator:
+    '''Combines the different methods that generate USFM from other formats in one class'''
+    def __init__(self):
+        self.usfm_string = ''
+
+    def is_valid_usfm(self, usfm_string: dict = None) -> bool:
+        '''Check the generated or passed USFM's correctness using the grammar'''
+        if usfm_string is None:
+            usfm_string = self.usfm_string
+        return False
+
+    def usj_to_usfm(self, usj_obj: dict, nested=False) -> None: # pylint: disable=too-many-statements, too-many-branches
+        '''Traverses through the dict/json and uses 'type' field to form USFM elements'''
+        marker_types = usj_obj['type'].split(':')
+        if usj_obj['type'] not in NO_USFM_USJ_TYPES:
+            self.usfm_string += "\\"
+            if nested and marker_types[0] == 'char':
+                self.usfm_string+="+"
+            self.usfm_string += f"{marker_types[-1]} "
+        if 'code' in usj_obj:
+            self.usfm_string += f"{usj_obj['code']} "
+        if 'number' in usj_obj:
+            self.usfm_string += usj_obj['number']
+            if marker_types[0] == "verse":
+                self.usfm_string += " "
+        if 'caller' in usj_obj:
+            self.usfm_string += f"{usj_obj['caller']} "
+        if 'category' in usj_obj:
+            self.usfm_string += f"\\cat {usj_obj['category']}\\cat*\n"
+        if 'content' in usj_obj:
+            for item in usj_obj['content']:
+                if isinstance(item, str):
+                    self.usfm_string += item
+                else:
+                    if marker_types[0] in ['char']:
+                        self.usj_to_usfm(item, nested=True)
+                    else:
+                        self.usj_to_usfm(item)
+        attributes = False
+        for key in usj_obj:
+            if key not in NON_ATTRIB_USJ_KEYS:
+                if not attributes:
+                    self.usfm_string += "|"
+                    attributes = True
+                if key == "file":
+                    self.usfm_string += f"src=\"{usj_obj[key]}\" "
+                else:
+                    self.usfm_string += f"{key}=\"{usj_obj[key]}\" "
+
+        if marker_types[0] in CLOSING_USJ_TYPES:
+            self.usfm_string = self.usfm_string.strip() + "\\"
+            if nested and marker_types[0] == 'char':
+                self.usfm_string+="+"
+            self.usfm_string += f"{marker_types[-1]}* "
+        if marker_types[0] == "ms":
+            if "sid" in usj_obj:
+                if not attributes:
+                    self.usfm_string += "|"
+                    attributes = True
+                self.usfm_string += f"sid=\"{usj_obj['sid']}\" "
+            self.usfm_string = self.usfm_string.strip() + "\\*"
+        if marker_types[0] == "sidebar":
+            self.usfm_string += "\\esbe"
+        if ":".join(marker_types[:-1]) not in NO_NEWLINE_USJ_TYPES and \
+            self.usfm_string[-1] != "\n":
+            self.usfm_string += "\n"
+        if "altnumber" in usj_obj:
+            self.usfm_string += f"\\{marker_types[-1]}a {usj_obj['altnumber']}"
+            self.usfm_string += f"\\{marker_types[-1]}a* "
+        if "pubnumber" in usj_obj:
+            self.usfm_string += f"\\{marker_types[-1]}p {usj_obj['pubnumber']}"
+            if marker_types[-1] == "v":
+                self.usfm_string += f"\\{marker_types[-1]}p* "
+            else:
+                self.usfm_string += "\n"
+
+    # def usx_to_usfm(self, usx_xml_tree) -> str: # should we call it just from_usx() instead
+    #     '''Traverses xml tree and converts nodes to usfm elements
+    #     based on type and style fields'''
+    #     return self.usfm_string
diff --git a/py-usfm-parser/src/usfm_grammar/usfm_parser.py b/py-usfm-parser/src/usfm_grammar/usfm_parser.py
index dccc1150..d257d436 100755
--- a/py-usfm-parser/src/usfm_grammar/usfm_parser.py
+++ b/py-usfm-parser/src/usfm_grammar/usfm_parser.py
@@ -10,6 +10,7 @@
 from usfm_grammar.usx_generator import USXGenerator
 from usfm_grammar.usj_generator import USJGenerator
 from usfm_grammar.list_generator import ListGenerator
+from usfm_grammar.usfm_generator import USFMGenerator
 from usfm_grammar.filters import exclude_markers_in_usj, include_markers_in_usj
 
 class Filter(list, Enum):
@@ -41,12 +42,13 @@ class Filter(list, Enum):
     # INNER_CONTENT = ['content-in-excluded-parent']
 
 class Format(str, Enum):
-    '''Defines the valid values for output formats'''
-    JSON = "json"
+    '''Defines the valid values for input and output formats'''
+    JSON = "usj"
     CSV = "table"
     ST = "syntax-tree"
     USX = "usx"
     MD = "markdown"
+    USFM = "usfm"
 
 lang_file = resources.path('usfm_grammar','my-languages.so')
 USFM_LANGUAGE = Language(str(lang_file), 'usfm3')
@@ -75,9 +77,19 @@ class Format(str, Enum):
 
 class USFMParser():
     """Parser class with usfmstring, syntax_tree and methods for JSON convertions"""
-    def __init__(self, usfm_string):
+    def __init__(self, usfm_string:str=None, from_usj:dict=None):
         # super(USFMParser, self).__init__()
-        self.usfm = usfm_string
+        if usfm_string is not None and from_usj is not None:
+            raise Exception("Found USFM and USJ inputs! Only one supported in one object.")
+        if usfm_string is not None:
+            self.usfm = usfm_string
+        elif from_usj is not None:
+            usj_converter = USFMGenerator()
+            usj_converter.usj_to_usfm(from_usj)
+            self.usfm = usj_converter.usfm_string
+        else:
+            raise Exception("Missing input! Either USFM or USJ to be provided.")
+
         self.usfm_bytes = None
         self.syntax_tree = None
         self.errors = None


Test patch:
diff --git a/py-usfm-parser/tests/__init__.py b/py-usfm-parser/tests/__init__.py
index 4f60de23..648b84cc 100644
--- a/py-usfm-parser/tests/__init__.py
+++ b/py-usfm-parser/tests/__init__.py
@@ -13,6 +13,16 @@ def initialise_parser(input_usfm_path):
     test_parser = USFMParser(usfm_string)
     return test_parser
 
+def generate_USFM_from_USJ(input_usj):
+    '''Create a generator, and use usj_to_usfm convertion API'''
+    usj_parser = USFMParser(from_usj=input_usj)
+    return usj_parser.usfm
+
+def parse_USFM_string(usfm_string):
+    '''Set up a parser obj with given string input'''
+    test_parser = USFMParser(usfm_string)
+    return test_parser
+
 def is_valid_usfm(input_usfm_path):
     '''Checks the metadata.xml to see is the USFM is a valid one'''
     if input_usfm_path in pass_fail_override_list:
diff --git a/py-usfm-parser/tests/test_json_conversion.py b/py-usfm-parser/tests/test_json_conversion.py
index ed5f096c..8fb0c9c6 100644
--- a/py-usfm-parser/tests/test_json_conversion.py
+++ b/py-usfm-parser/tests/test_json_conversion.py
@@ -4,7 +4,7 @@
 from jsonschema import validate
 
 from tests import all_usfm_files, initialise_parser, doubtful_usfms, negative_tests,\
-    find_all_markers, Filter, exclude_USX_files
+    find_all_markers, Filter, generate_USFM_from_USJ, parse_USFM_string, exclude_USX_files
 
 all_valid_markers = []
 for member in Filter:
@@ -17,7 +17,7 @@
 
 @pytest.mark.parametrize('file_path', test_files)
 @pytest.mark.timeout(30)
-def test_dict_converions_without_filter(file_path):
+def test_usj_converions_without_filter(file_path):
     '''Tests if input parses without errors'''
     test_parser = initialise_parser(file_path)
     assert not test_parser.errors, test_parser.errors
@@ -31,7 +31,7 @@ def test_dict_converions_without_filter(file_path):
                             Filter.TITLES+Filter.BOOK_HEADERS
                         ])
 @pytest.mark.timeout(30)
-def test_dict_converions_with_exclude_markers(file_path, exclude_markers):
+def test_usj_converions_with_exclude_markers(file_path, exclude_markers):
     '''Tests if input parses without errors'''
     test_parser = initialise_parser(file_path)
     assert not test_parser.errors, test_parser.errors
@@ -48,7 +48,7 @@ def test_dict_converions_with_exclude_markers(file_path, exclude_markers):
                             Filter.TITLES+Filter.BOOK_HEADERS
                         ])
 @pytest.mark.timeout(30)
-def test_dict_converions_with_include_markers(file_path, include_markers):
+def test_usj_converions_with_include_markers(file_path, include_markers):
     '''Tests if input parses without errors'''
     test_parser = initialise_parser(file_path)
     assert not test_parser.errors, test_parser.errors
@@ -88,7 +88,7 @@ def get_types(element):
 
 @pytest.mark.parametrize('file_path', test_files)
 @pytest.mark.timeout(30)
-def test_all_markers_are_in_output(file_path):
+def test_usj_all_markers_are_in_output(file_path):
     '''Tests if all markers in USFM are present in output also'''
     test_parser = initialise_parser(file_path)
     assert not test_parser.errors, test_parser.errors
@@ -110,13 +110,27 @@ def test_all_markers_are_in_output(file_path):
 
 @pytest.mark.parametrize('file_path', test_files)
 @pytest.mark.timeout(30)
-def test_output_is_valid_usj(file_path):
+def test_usj_output_is_valid(file_path):
     '''Test generated USJ against USJ schema'''
     test_parser = initialise_parser(file_path)
     assert not test_parser.errors, test_parser.errors
     usj_dict = test_parser.to_usj()
     validate(instance=usj_dict, schema=USJ_SCHEMA)
 
+@pytest.mark.parametrize('file_path', test_files)
+@pytest.mark.timeout(30)
+def test_usj_round_tripping(file_path):
+    '''Convert USFM to USJ and back to USFM. Compare first USFM and second USFM based on parse tree''' 
+    test_parser1 = initialise_parser(file_path)
+    assert not test_parser1.errors, test_parser1.errors
+    usj_dict = test_parser1.to_usj()
+
+    generated_USFM = generate_USFM_from_USJ(usj_dict)
+    test_parser2 = parse_USFM_string(generated_USFM)
+    assert not test_parser2.errors, str(test_parser2.errors)#+"\n"+ generated_USFM
+
+    # assert test_parser1.to_syntax_tree() == test_parser2.to_syntax_tree(), generated_USFM
+
 def remove_newlines_in_text(usj_dict):
     '''The test samples in testsuite do not preserve new lines in. But we do in usfm-grammar.
     So removing them just for comparison'''