Giter Site home page Giter Site logo

bridgeconn / usfm-grammar Goto Github PK

View Code? Open in Web Editor NEW
36.0 8.0 14.0 16.09 MB

An elegant USFM parser.

Home Page: https://usfm-grammar-revant.netlify.app/

License: MIT License

JavaScript 100.00%
parser-generator grammar usfm-validator usfm javascript usfm-converter usfm-grammar parser usfm-json scripture-open-components

usfm-grammar's Introduction

USFM Grammar

🚨If you have not already, please consider filling-up this short 2-minute survey to make usfm-grammar even better: 👉 link

An elegant USFM parser (or validator) that uses a parsing expression grammar to model USFM. The grammar is written using ohm. Supports USFM 3.x.

The parsed USFM is an intuitive and easy to manipulate JSON structure that allows for painless extraction of scripture and other content from the markup. USFM Grammar is also capable of reconverting the generated JSON back to USFM.

Currently, the parser is implemented in JavaScript. But it is possible to re-use the grammar and port this library into other programming languages too. Contributions are welcome!

Note: Refer the docs for more information like the disclaimer, release notes, etc.

Features

  • USFM validation
  • USFM to JSON convertor with 2 different levels of strictness
  • JSON to USFM convertor
  • CSV/TSV converter for both USFM and JSON
  • Command Line Interface (CLI)

Try it out!

Try out the usfm-grammar based online convertor: https://usfm-grammar-revant.netlify.app/

Example

Input USFMParsed JSON OutputParsed JSON with only filtered Scripture Content
\id hab 45HABGNT92.usfm, Good News Translation, June 2003
\c 3
\s1 A Prayer of Habakkuk
\p
\v 1 This is a prayer of the prophet Habakkuk:
\b
\q1
\v 2 O \nd Lord\nd*, I have heard of what you have done,
\q2 and I am filled with awe.
\q1 Now do again in our times
\q2 the great deeds you used to do.
\q1 Be merciful, even when you are angry.
{
  "book": {    "bookCode": "HAB",
          "description": "45HABGNT92.usfm, Good News Translation, June 2003"  },
  "chapters": [
    {"chapterNumber": "3",
      "contents": [
        [ { "s1": "A Prayer of Habakkuk" } ],
        { "p": null },
        { "verseNumber": "1",
          "verseText": "This is a prayer of the prophet Habakkuk:",
          "contents": [
            "This is a prayer of the prophet Habakkuk:",
            { "b": null },
            { "q1": null }  ] },
        { "verseNumber": "2",
          "verseText": "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.",
          "contents": [
            "O",
            { "nd": [ "Lord" ],
              "closing": "\\nd*" },
            ", I have heard of what you have done,",
            { "q2": null },
            "and I am filled with awe.",
            { "q1": null },
            "Now do again in our times",
            { "q2": null },
            "the great deeds you used to do.",
            { "q1": null },
            "Be merciful, even when you are angry." ] }
      ]
    }
  ],
  "_messages": {
    "_warnings": [ "Book code is in lowercase." ] }
}

{ "book": { "bookCode": "HAB",
        "description": "45HABGNT92.usfm, Good News Translation, June 2003" },
  "chapters": [
    { "chapterNumber": "3",
      "contents": [
        { "verseNumber": "1",
          "verseText": "This is a prayer of the prophet Habakkuk:" },
        { "verseNumber": "2",
          "verseText": "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." }
      ]
    }
  ],
  "_messages": {
    "_warnings": [ "Book code is in lowercase. " ]
  }
}

The converted JSON structure adheres to the JSON Schema defined here.

The converted JSON uses USFM marker names as its property names along with the following additional names:
book, bookCode, description, meta, chapters, contents, verseNumber, verseText, attributes, defaultAttribute, closing, footnote, endnote, extended-footnote, cross-ref, extended-cross-ref, caller (used within notes), list, table, header (used within table), milestone and namespace.

Installation

The parser is available on NPM and can be installed by:

npm install usfm-grammar

Usage

Command Line Interface (CLI)

To use this tool from the command line install it globally like:

npm install -g usfm-grammar

Then from the command line (terminal) to convert a valid USFM file into JSON (on stdout) run:

usfm-grammar /path/to/file.usfm

$ usfm-grammar -h
usfm-grammar <file>

Parse/validate USFM 3.x to/from JSON.

Positionals:
  file  The path of the USFM or JSON file to be parsed and/or converted. By
        default, auto-detects input USFM and converts it into JSON and
        vice-versa.

Options:
  -l, --level    Level of strictness in parsing. This defaults to `strict`.
                                                            [choices: "relaxed"]
      --filter   Filter out content from input USFM. Not applicable for input
                 JSON or for CSV/TSV output.              [choices: "scripture"]
  -o, --output   The output format to convert input into.
                                         [choices: "csv", "tsv", "usfm", "json"]
  -h, --help     Show help                                             [boolean]
  -v, --version  Show version number                                   [boolean]

The options -l (--level) and --filter do not have any effect if used for JSON to USFM conversion.

JavaScript APIs

USFM to JSON

  1. USFMParser.toJSON()
  2. USFMParser.toJSON(grammar.FILTER.SCRIPTURE)
const grammar = require('usfm-grammar');

var input = '\\id PSA\n\\c 1\n\\p\n\\v 1 Blessed is the one who does not walk in step with the wicked or stand in the way that sinners take or sit in the company of mockers,';

const myUsfmParser = new grammar.USFMParser(input);

// Returns JSON representation of a valid input USFM string
var jsonOutput = myUsfmParser.toJSON();

// Returns a simplified (scripture-only) JSON representation while excluding other USFM content
var scriptureJsonOutput = myUsfmParser.toJSON(grammar.FILTER.SCRIPTURE);

Note
If you intend to re-convert a USFM from the generated JSON, we recommend using .toJSON() without the grammar.FILTER.SCRIPTURE option in order to retain all information of the original USFM file.

relaxed Mode
There is high chance that a USFM file you encounter in the wild is not fully valid according to the specifications. In order to accomodate such cases and provide a parse-able output to work with we created a relaxed mode. This maybe used as shown:

const myRelaxedUsfmParser = new grammar.USFMParser(input, grammar.LEVEL.RELAXED);
var jsonOutput = myRelaxedUsfmParser.toJSON();

The relaxed mode provides relaxation from checking several rules in the USFM specifcation. It tries hard to accomodate non-standard USFM markup and attempts to generate a JSON output for it. Only the most important markers are checked for, like the \id at the start, presence of \c and \v markers. Though all the markers in the input USFM file are preserved in the generated JSON output, their syntax or their positions in the file is not verified for correctness. Even misspelled markers would be accepted!

Caution: Errors may go unnoticed that might lead to loss of information when using the relaxed mode. For example, if the input USFM has erroneously does not have a space between the verse marker and the verse number (e.g. \v3) the parser in relaxed mode would treat it as a separate marker (v3 as opposed to v) and fail to recognise it is a verse. The right (or the hard) thing to do is fix the markup according to the specification. We generally recommend using the grammar in the default mode.

Validate USFM

  1. USFMParser.validate()
// Returns a Boolean indicating whether the input USFM text satisfies the grammar or not. 
// This method is available in both default and relaxed modes.
var isUsfmValid = myUsfmParser.validate();

JSON to USFM

Note

  • The input JSON should have been generated by usfm-grammar (or in the same format).
  • If a USFM file is converted to JSON and then back to USFM, the re-created USFM will have the same contents but spacing and new-lines will be normalized.
  1. JSONParser.toUSFM()
const myJsonParser = new grammar.JSONParser(jsonOutput);

// Returns the original USFM that was previously converted to JSON
let reCreatedUsfm = myJsonParser.toUSFM();

This method works with JSON output created with or without the grammar.FILTER.SCRIPTURE option.

Validate JSON

  1. JSONParser.validate()
// Returns a Boolean indicating whether the input JSON confines to grammar.JSONSchemaDefinition. 
var isJsonValid = myJsonParser.validate();

USFM/JSON to CSV/TSV

  1. USFMParser.toCSV()

  2. JSONParser.toCSV()

  3. USFMParser.toTSV()

  4. JSONParser.toTSV()

// Example usage:
// Returns CSV and TSV from a USFM, respectively
var csvString = myUsfmParser.toCSV();
var tsvString = myUsfmParser.toTSV();

The toCSV() and toTSV() methods return a tabular representation of the verses in the format:

<BOOK, CHAPTER, VERSE-NUMBER, VERSE-TEXT>

usfm-grammar's People

Contributors

aunger avatar beniza avatar dependabot[bot] avatar jcuenod avatar joelthe1 avatar kavitharaju avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

usfm-grammar's Issues

"Undefined" coming in verse['text'] of verses with `\li#` marker

The text property of the verse object is where the whole text of the verse is coming without any markers or metadata.
For a verse with list in it, it is not coming as expected.
It has happened after the list json structure changed for reverse convertability.

fix the bugs in the beta2 release

  • the \usfm marker should be allowed even below \ide and similar markers
  • the \p at the chapter start is still not mandatory, make it mandatory

Error while parsing file

Unable to parse https://git.door43.org/unfoldingWord/el-x-koine_ugnt/raw/branch/master/50-EPH.usfm

{
  "ERROR": "Line 1797, col 49:\n  1796 | \\w γὰρ|lemma=\"γάρ\" strong=\"G10630\" x-morph=\"Gr,CC,,,,,,,,\"\\w*\n> 1797 | \\k-s | x-tw=\"rc://*/tw/dict/bible/other/know\" \\w*\n   
^\n  1798 | \\w ἴστε|lemma=\"εἴδω\" strong=\"G14920\" x-morph=\"Gr,V,MEA2,,P,\" \\w*,\nExpected \"\\\\\", \"-e\", \"-s\", or \" \""
}

Also, we should change the key to _error. And is it possible to have this in the same structure as the _warnings like

"_messages": {
    "_warnings": [
      "Empty lines present. "
    ]
  }

Verse text is parsed incorrectly while in relaxed mode

While parsing the following USFM using this setup new grammar.USFMParser( data, grammar.LEVEL.RELAXED );:

\c 1
\s1 Сотворение мира
\p
\v 1 В начале Всевышний\f + \fr 1:1 \fk Всевышний \ft – на языке оригинала: «Элохим» – слово, родственное арабскому «Аллах». См. приложение V.\f* сотворил небо и землю.

getting this JSON output:

"chapters": [
    {
      "chapterNumber": "1",
      "contents": [
        {
          "s1": "Сотворение мира"
        },
        {
          "p": null
        },
        {
          "verseNumber": "1",
          "contents": [
            "В начале Всевышний",
            {
              "f": "+"
            },
            {
              "fr": "1:1"
            },
            {
              "fk": [
                "Всевышний",
                {
                  "ft": "– на языке оригинала: «Элохим» – слово, родственное арабскому «Аллах». См. приложение V.",
                  "closing": "\\f*"
                },
                "сотворил небо и землю."
              ]
            }
          ],
          "verseText": "В начале Всевышний"
        },

notice, how a part of the verse text - "сотворил небо и землю." - became a part of "fk": array

error at new line in between attributes

The markers are not expected to take newline within them(except verse marker).

The following sample throws error due to a new line in between the atributes on milestone marker

\p
\v 22 Paul stood up in front of the city council and said, \qt1-s |sid="qt1_ACT_17:22"
who="Paul"\*“I see that in every way you Athenians are very religious.
\v 23 For as I walked through your city and looked at the places where you worship,

Re-design the JSON output

Currently there are 3 main JSON outputs.

  1. Output of normal usfm parsing
  2. Output of USFM parsing in SCRIPTURE only mode
  3. Output of relaxed mode USFM parsing

The reverse parse is built based on 1.

The new JSON structure should be common to all these. Also must usable to re-create the USFM (reverse parse).

use // to force a new line

I just saw the following line in a discussion. Because it was shared by an expert in usfm, I think it is a valid case.

to force line breaks--for which // is the appropriate SFM.

Does your grammar support this?

Handling Split Verses

In some Bibles verses are split into two (or more) sub parts. Please see an example from GNT below:
image

The code handling verse number doesn't seem to handle this case.

verseNumber = number ("-" number)? spaceOrNewLine

cannot make newlLine mandatory for paragraph markers

As per the spec all paragraph markers should come on a newline https://ubsicap.github.io/usfm/about/syntax.html?highlight=newline#id5

But the rule in the Grammar was relaxed to accomodate the example is test cases(from paratext and usfm-js). Now making it mandatory leads to these test cases failing.

20 tests are failing in total. Most of them are from paratext test cases and a few from usfm-js's set and a few from our basic tests.

@joelthe1 please recommend how to go about this.
Should we make the change and update these test cases to adhere to the spec?

First stable release

After testing the beta and making required updations, publish a new stable version of usfm-grammar library

Setup SSL certificate

Currently usfm.bridgeconn.com does not have an SSL certificate setup for it. We should use LetsEncrypt to set it up.

Release Beta

Release a beta version of the grammar that can be used as an NPM module.

Throwing error at \q within \d

\id GEN
\c 56
\s ପରମେଶ୍ୱରଙ୍କ ଉପରେ ନିର୍ଭରଶୀଳତାର ପ୍ରାର୍ଥନା
\d ପ୍ରଧାନ ବାଦ୍ୟକର ନିମନ୍ତେ ଯୋନତ୍‍-ଏଲମ୍‍
\q ରହୋକୀମ୍‍ ସ୍ୱରରେ ଦାଉଦଙ୍କର ଗୀତ। ମିକ୍ତାମ୍‍; ପଲେଷ୍ଟୀୟମାନେ ତାଙ୍କୁ ଗାଥ୍‍ ନଗରରେ ଧରିବା ସମୟରେ ରଚିତ।
\q
\v 1 ହେ ପରମେଶ୍ୱର, ମୋତେ ଦୟା କର; କାରଣ ମୋହର ଶତ୍ରୁ ମୋତେ ଗ୍ରାସ କରିବାକୁ ଉଦ୍ୟତ;
\q ସେ ସାରାଦିନ ଯୁଦ୍ଧ କରି ମୋ’ ପ୍ରତି ଉପଦ୍ରବ କରଇ।
\q

giving error

{
ERROR: "Line 5, col 4:
4 | \d ପ୍ରଧାନ ବାଦ୍ୟକର ନିମନ୍ତେ ଯୋନତ୍‍-ଏଲମ୍‍
> 5 | \q ରହୋକୀମ୍‍ ସ୍ୱରରେ ଦାଉଦଙ୍କର ଗୀତ। ମିକ୍ତାମ୍‍; ପଲେଷ୍ଟୀୟମାନେ ତାଙ୍କୁ ଗାଥ୍‍ ନଗରରେ ଧରିବା ସମୟରେ ରଚିତ।
^
6 | \q
Expected "\""
}

Grammar fails to parse very large files

when tested using a large alignment USFM file of size 2.6 MB, the library fails with the following error

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Cannot parse q1 tag when it is part of the chapter header and has text

Cannot parse this part \q1 Славьте Вечного! in the following text:

\id PSA - (SOMEVersions) 
\rem Copyright © 2003, 2009, 2013 .®
\h Забур
\toc1 Забур
\toc2 Забур
\toc3 Заб.
\mt1 Забур
\imt – введение
\ip Книга Забур состоит из ста пятидесяти песен, которые использовались как во время общих богослужений, так и для личной молитвы Всевышнему. Здесь есть песни различных видов: прославления, плачи, благодарения; песни раскаяния и упования на Всевышнего; песни о славе Иерусалима и песни для паломничества в храм; царские гимны (которые исполнялись, например, на коронациях), учительные песни, а также литургические песнопения. В песнях Забура часто выражены глубокие чувства и переживания. Здесь звучит не только радость, хвала и доверие Всевышнему, но и отчаяние, гнев, раскаяние и страх перед врагами. Многие из песен Забура пророчествуют о приходе Исы Масиха. Например, в песнях \xt 2\xt* и \xt 109\xt* Он описывается как Правитель от Всевышнего на земле, а в песни \xt 21\xt* предсказываются Его страдания за грехи всего человечества.
\ip Забур составлен очень искусно, и, чтобы читатель мог лучше его оценить, стоит сделать несколько примечаний о его структуре. Он разделён на пять книг (песни \xt 1–40; 41–71; 72–88; 89–105; 106–150\xt*), видимо, в подражание пяти книгам Таурата. Каждая часть заканчивается благословением. Песнь \xt 1\xt* служит введением для всей книги, а Песнь \xt 150\xt* – это заключительное благословение пятой части и всей книги Забур. Во всей книге есть части, которые изначально были отдельными сборниками. Это, например, песни восхождения (\xt 119–133\xt*) или песни Асафа (\xt 72–82\xt*). Структура каждой песни хорошо продумана, что лучше всего видно в акростихах (песни \xt 9\xt*, \xt 24\xt*, \xt 33\xt*, \xt 36\xt*, \xt 110\xt*, \xt 111\xt*, \xt 118\xt*, \xt 144\xt*).
\ip В большинстве случаев в начале песни есть заглавие с указаниями о манере исполнения, с информацией об авторе, жанре или историческом контексте. Так как некоторые из древних терминов, находящихся в заглавиях, сейчас плохо понятны, они могут быть переведены только приблизительно. Существует также мнение, что в некоторых случаях в начале песен упомянут не автор, а тот, о ком написана данная песнь, или кому она посвящена.
\ip Забур – душа Священного Писания – является любимой молитвенной книгой народа Всевышнего во всех поколениях.
\iot Содержание
\io1 Первая книга (\ior Песни 1–40\ior*)
\io1 Вторая книга (\ior Песни 41–71\ior*)
\io1 Третья книга (\ior Песни 72–88\ior*)
\io1 Четвёртая книга (\ior Песни 89–105\ior*)
\io1 Пятая книга (\ior Песни 106–150\ior*)
\ie
\c 1
\ms Первая книга
\cl Песнь 1
\q1
\v 1 Благословен человек,
\q2 который не следует совету нечестивых,
\q1 не ходит путями грешников
\q2 и не сидит в собрании насмешников,
\q1
\v 2 но в Законе Вечного\f + \fr 1:2 \fk Вечный \ft – на языке оригинала: «Яхве». Под этим именем Всевышний открылся Мусе и народу Исраила (см. \+xt Исх. 3:13-15\+xt*). См. пояснительный словарь.\f* находит радость
\q2 и о Законе Его размышляет день и ночь.
\q1
\v 3 Он как дерево, посаженное у потоков вод,
\q2 которое приносит плод в своё время,
\q2 и чей лист не вянет.
\q1 Что бы он ни сделал, во всём преуспеет.
\b
\q1
\v 4 Не таковы нечестивые!
\q2 Они как мякина,
\q2 которую гонит ветер.
\q1
\v 5 Поэтому не устоят на суде нечестивые,
\q2 и грешники – в собрании праведных.
\b
\q1
\v 6 Ведь Вечный охраняет путь праведных,
\q2 а путь нечестивых погибнет.
\c 110
\cl Песнь 110\f + \fr 110 \fl Песнь 110 \ft В оригинале эта песнь написана в форме акростиха: каждая строка начинается с очередной буквы еврейского алфавита.\f*
\q1 Славьте Вечного!
\b
\q1
\v 1 Славлю Вечного всем своим сердцем
\q2 в совете праведных и в собрании народном.
\b
\q1

Supply user with warnings

In case, parsing causes an error, we should fail. Otherwise, parsing should return converted JSON along with warnings, if any. This can be done restructuring the API to take a parameter for storing error and warnings.

Create a relax mode for parsing

Write a simpler grammar, with more generalized rules to model USFM
It should be able to handle USFM 1.x, 2.x and 3.x
And should successfully parse for minor error which the regular mode would reject

Support for CSV/TSV Export

There are several useful use cases of having a USFM file in a CSV or Tab Separated format. (Tab is preferred as the natural text may contain several commas in it). Could you please provide an export option?

Also, please include the errors or warnings encountered during the validation/conversion process.

'Happy Path' Testing

Write tests for about 90% of the markers that check the if the grammar behaves when given proper input USFM.

How do you generate usfm.ohm.js file?

I wonder how do you generate usfm.ohm.js file? I understand that it has as its value (exports.contents = ) the content of the usfm.ohm file.

can anything like the following to work as well?

const contents = fs.readFileSync(
  '../grammar/usfm.ohm',
);

instead of this:

const { contents } = require("../grammar/usfm.ohm.js");

Please let me know

Text normalization not working sometimes on the demo site

When there is book code in lower case, the text normalization stage is supposed to change the case and generate a warning.

It is working sometimes. But sometimes it is seen that the normalization is not happening and a lower case book code is treated as an error.

Compare with usfm-js

  • Compare the time taken by both libraries
  • Also compare the JSON structure generated by both

add offset for verse-metadata contents

The current JSON structure separate metadata from verse text. So if we are to re-construct the usfm from the output JSON, we need to know where in the verse text, the separated metadata would have to be attached back.
In order to facilitate that, add an additional key/field with the metadata elements showing their offset in the verse text.
Would be better if it is byte offset.

Add CLI

Add a Command Line Interface for USFM-grammar

Parse English ESV

See the errors in the ESV bible parsing to see and see if any change is required in the code and fix them as required.

Allow \s5 markers

Allow the section marker \s to take any number
Allow it to have empty text

Trailing spaces on line showing error

The normalization of trailing spaces not working as expected sometimes.

The trailing space regex pattern should be updated to include multiple trailing spaces at line end
/ +[\n\r]/g

Cannot use from React

The usfm-grammar uses fs library and accesses files. so it cannot be used from client-side.

Change that by avoiding the use of fs

restructure the code

Re-structure the code. make each module(USFM parser and JSON parser) a class. have a superclass parser that is inherited by the modules.
Make APIs standardized and more intuitive.

Prepare Change log

As the JSON structure is going through several changes as part of enabling reverse conversion possible, all the changes brought in should be documented.

Spot check with USFM files from the wild

As a another level of testings, let us collect random USFM files we can collect from multiple sources and attempt to parse them to see if our parser can take it.

Report empty Paragraph markers

Currently, the grammar permits the use of repeated empty paragraph markers. This shouldn't be allowed as it doesn't make any sense. Please see the eg. below

\id HAB 45HABGNT92.usfm, Good News Translation, June 2003
\c 3
\s1 A Prayer of Habakkuk
\p
\p
\v 1 This is a prayer of the prophet Habakkuk:
\b
\q1
\m
\p
\p 
\q 
\s 
\p
\v 2 O \nd Lord\nd*, I have heard of what you have done,
\q2 and I am filled with awe.
\q1 Now do again in our times
\q2 the great deeds you used to do.
\q1 Be merciful, even when you are angry.

If this happens, the system should report it as an issue.

parse internal contents of notes

The current JSON shows the internal contents of foot notes and cross references as a single string.
Break it down to separate markers and text.

Online testing page

Would it be possible to setup an online page where we can test the USFM grammar against various files?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.