` is any of the usual `conventional-changelog` prefixes, at your discretion.
136 | * Go to https://github.com/iarna/rtf-to-html/pulls and open a new pull request with your changes.
137 | * If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing.
138 |
139 | Once you've filed the PR:
140 |
141 | * Barring special circumstances, maintainers will not review PRs until all checks pass (Travis, AppVeyor, etc).
142 | * One or more maintainers will use GitHub's review feature to review your PR.
143 | * If the maintainer asks for any changes, edit your changes, push, and ask for another review. Additional tags (such as `needs-tests`) will be added depending on the review.
144 | * If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. 💚
145 | * If your PR gets accepted, it will be marked as such, and merged into the `latest` branch soon after. Your contribution will be distributed to the masses next time the maintainers [tag a release](#tag-a-release)
146 |
147 | ## Provide Support on Issues
148 |
149 | [Needs Collaborator](#join-the-project-team): none
150 |
151 | Helping out other users with their questions is a really awesome way of contributing to any community. It's not uncommon for most of the issues on an open source projects being support-related questions by users trying to understand something they ran into, or find their way around a known bug.
152 |
153 | Sometimes, the `support` label will be added to things that turn out to actually be other things, like bugs or feature requests. In that case, suss out the details with the person who filed the original issue, add a comment explaining what the bug is, and change the label from `support` to `bug` or `feature`. If you can't do this yourself, @mention a maintainer so they can do it.
154 |
155 | In order to help other folks out with their questions:
156 |
157 | * Go to the issue tracker and [filter open issues by the `support` label](https://github.com/iarna/rtf-to-html/issues?q=is%3Aopen+is%3Aissue+label%3Asupport).
158 | * Read through the list until you find something that you're familiar enough with to give an answer to.
159 | * Respond to the issue with whatever details are needed to clarify the question, or get more details about what's going on.
160 | * Once the discussion wraps up and things are clarified, either close the issue, or ask the original issue filer (or a maintainer) to close it for you.
161 |
162 | Some notes on picking up support issues:
163 |
164 | * Avoid responding to issues you don't know you can answer accurately.
165 | * As much as possible, try to refer to past issues with accepted answers. Link to them from your replies with the `#123` format.
166 | * Be kind and patient with users -- often, folks who have run into confusing things might be upset or impatient. This is ok. Try to understand where they're coming from, and if you're too uncomfortable with the tone, feel free to stay away or withdraw from the issue. (note: if the user is outright hostile or is violating the CoC, [refer to the Code of Conduct](CODE_OF_CONDUCT.md) to resolve the conflict).
167 |
168 | ## Label Issues
169 |
170 | [Needs Collaborator](#join-the-project-team): Issue Tracker
171 |
172 | One of the most important tasks in handling issues is labeling them usefully and accurately. All other tasks involving issues ultimately rely on the issue being classified in such a way that relevant parties looking to do their own tasks can find them quickly and easily.
173 |
174 | In order to label issues, [open up the list of unlabeled issues](https://github.com/iarna/rtf-to-html/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) and, **from newest to oldest**, read through each one and apply issue labels according to the table below. If you're unsure about what label to apply, skip the issue and try the next one: don't feel obligated to label each and every issue yourself!
175 |
176 | Label | Apply When | Notes
177 | --- | --- | ---
178 | `bug` | Cases where the code (or documentation) is behaving in a way it wasn't intended to. | If something is happening that surprises the *user* but does not go against the way the code is designed, it should use the `enhancement` label.
179 | `critical` | Added to `bug` issues if the problem described makes the code completely unusable in a common situation. |
180 | `documentation` | Added to issues or pull requests that affect any of the documentation for the project. | Can be combined with other labels, such as `bug` or `enhancement`.
181 | `duplicate` | Added to issues or PRs that refer to the exact same issue as another one that's been previously labeled. | Duplicate issues should be marked and closed right away, with a message referencing the issue it's a duplicate of (with `#123`)
182 | `enhancement` | Added to [feature requests](#request-a-feature), PRs, or documentation issues that are purely additive: the code or docs currently work as expected, but a change is being requested or suggested. |
183 | `help wanted` | Applied by [Committers](#join-the-project-team) to issues and PRs that they would like to get outside help for. Generally, this means it's lower priority for the maintainer team to itself implement, but that the community is encouraged to pick up if they so desire | Never applied on first-pass labeling.
184 | `in-progress` | Applied by [Committers](#join-the-project-team) to PRs that are pending some work before they're ready for review. | The original PR submitter should @mention the team member that applied the label once the PR is complete.
185 | `performance` | This issue or PR is directly related to improving performance. |
186 | `refactor` | Added to issues or PRs that deal with cleaning up or modifying the project for the betterment of it. |
187 | `starter` | Applied by [Committers](#join-the-project-team) to issues that they consider good introductions to the project for people who have not contributed before. These are not necessarily "easy", but rather focused around how much context is necessary in order to understand what needs to be done for this project in particular. | Existing project members are expected to stay away from these unless they increase in priority.
188 | `support` | This issue is either asking a question about how to use the project, clarifying the reason for unexpected behavior, or possibly reporting a `bug` but does not have enough detail yet to determine whether it would count as such. | The label should be switched to `bug` if reliable reproduction steps are provided. Issues primarily with unintended configurations of a user's environment are not considered bugs, even if they cause crashes.
189 | `tests` | This issue or PR either requests or adds primarily tests to the project. | If a PR is pending tests, that will be handled through the [PR review process](#review-pull-requests)
190 | `wontfix` | Labelers may apply this label to issues that clearly have nothing at all to do with the project or are otherwise entirely outside of its scope/sphere of influence. [Committers](#join-the-project-team) may apply this label and close an issue or PR if they decide to pass on an otherwise relevant issue. | The issue or PR should be closed as soon as the label is applied, and a clear explanation provided of why the label was used. Contributors are free to contest the labeling, but the decision ultimately falls on committers as to whether to accept something or not.
191 |
192 | ## Clean Up Issues and PRs
193 |
194 | [Needs Collaborator](#join-the-project-team): Issue Tracker
195 |
196 | Issues and PRs can go stale after a while. Maybe they're abandoned. Maybe the team will just plain not have time to address them any time soon.
197 |
198 | In these cases, they should be closed until they're brought up again or the interaction starts over.
199 |
200 | To clean up issues and PRs:
201 |
202 | * Search the issue tracker for issues or PRs, and add the term `updated:<=YYYY-MM-DD`, where the date is 30 days before today.
203 | * Go through each issue *from oldest to newest*, and close them if **all of the following are true**:
204 | * not opened by a maintainer
205 | * not marked as `critical`
206 | * not marked as `starter` or `help wanted` (these might stick around for a while, in general, as they're intended to be available)
207 | * no explicit messages in the comments asking for it to be left open
208 | * does not belong to a milestone
209 | * Leave a message when closing saying "Cleaning up stale issue. Please reopen or ping us if and when you're ready to resume this. See https://github.com/iarna/rtf-to-html/blob/latest/CONTRIBUTING.md#clean-up-issues-and-prs for more details."
210 |
211 | ## Review Pull Requests
212 |
213 | [Needs Collaborator](#join-the-project-team): Issue Tracker
214 |
215 | While anyone can comment on a PR, add feedback, etc, PRs are only *approved* by team members with Issue Tracker or higher permissions.
216 |
217 | PR reviews use [GitHub's own review feature](https://help.github.com/articles/about-pull-request-reviews/), which manages comments, approval, and review iteration.
218 |
219 | Some notes:
220 |
221 | * You may ask for minor changes ("nitpicks"), but consider whether they are really blockers to merging: try to err on the side of "approve, with comments".
222 | * *ALL PULL REQUESTS* should be covered by a test: either by a previously-failing test, an existing test that covers the entire functionality of the submitted code, or new tests to verify any new/changed behavior. All tests must also pass and follow established conventions. Test coverage should not drop, unless the specific case is considered reasonable by maintainers.
223 | * Please make sure you're familiar with the code or documentation being updated, unless it's a minor change (spellchecking, minor formatting, etc). You may @mention another project member who you think is better suited for the review, but still provide a non-approving review of your own.
224 | * Be extra kind: people who submit code/doc contributions are putting themselves in a pretty vulnerable position, and have put time and care into what they've done (even if that's not obvious to you!) -- always respond with respect, be understanding, but don't feel like you need to sacrifice your standards for their sake, either. Just don't be a jerk about it?
225 |
226 | ## Merge Pull Requests
227 |
228 | [Needs Collaborator](#join-the-project-team): Committer
229 |
230 | TBD - need to hash out a bit more of this process.
231 |
232 | ## Tag A Release
233 |
234 | [Needs Collaborator](#join-the-project-team): Committer
235 |
236 | TBD - need to hash out a bit more of this process. The most important bit here is probably that all tests must pass, and tags must use [semver](https://semver.org).
237 |
238 | ## Join the Project Team
239 |
240 | ### Ways to Join
241 |
242 | There are many ways to contribute! Most of them don't require any official status unless otherwise noted. That said, there's a couple of positions that grant special repository abilities, and this section describes how they're granted and what they do.
243 |
244 | All of the below positions are granted based on the project team's needs, as well as their consensus opinion about whether they would like to work with the person and think that they would fit well into that position. The process is relatively informal, and it's likely that people who express interest in participating can just be granted the permissions they'd like.
245 |
246 | You can spot a collaborator on the repo by looking for the `[Collaborator]` or `[Owner]` tags next to their names.
247 |
248 | Permission | Description
249 | --- | ---
250 | Issue Tracker | Granted to contributors who express a strong interest in spending time on the project's issue tracker. These tasks are mainly [labeling issues](#label-issues), [cleaning up old ones](#clean-up-issues-and-prs), and [reviewing pull requests](#review-pull-requests), as well as all the usual things non-team-member contributors can do. Issue handlers should not merge pull requests, tag releases, or directly commit code themselves: that should still be done through the usual pull request process. Becoming an Issue Handler means the project team trusts you to understand enough of the team's process and context to implement it on the issue tracker.
251 | Committer | Granted to contributors who want to handle the actual pull request merges, tagging new versions, etc. Committers should have a good level of familiarity with the codebase, and enough context to understand the implications of various changes, as well as a good sense of the will and expectations of the project team.
252 | Admin/Owner | Granted to people ultimately responsible for the project, its community, etc.
253 |
254 | ## Attribution
255 |
256 | This guide was generated using the WeAllJS `CONTRIBUTING.md` generator. [Make your own](https://npm.im/weallcontribute)!
257 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Rebecca Turner
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @iarna/rtf-to-html
2 |
3 | Convert RTF to HTML in pure JavaScript.
4 |
5 | ```js
6 | const rtfToHTML = require('@iarna/rtf-to-html')
7 |
8 | fs.createReadStream('example.rtf').pipe(rtfToHTML((err, html) => {
9 | // …
10 | })
11 | rtfToHTML.fromStream(fs.createReadStream('example.rtf'), (err, html) => {
12 | // …
13 | })
14 | rtfToHTML.fromString('{\\rtf1\\ansi\\b hi there\\b0}', (err, html) => {
15 | console.log(html)
16 | // prints a document containing:
17 | // hi there
18 | })
19 | ```
20 |
21 | This is built on [`rtf-parser`](https://www.npmjs.com/package/rtf-parser)
22 | and shares its limitations.
23 |
24 | This generates complete HTML documents from RTF documents. It does not
25 | currently have the facility to work on snippets of either.
26 |
27 | Supported features:
28 |
29 | * Paragraph detection (results in `` tags) plus empty paragraph trimming.
30 | * Font (as `font-family: Font Name, Font Family`). Font families in RTF
31 | don't map perfectly to HTML. This is the mapping we currently use:
32 | * roman: serif
33 | * swiss: sans-serif
34 | * script: cursive
35 | * decor: fantasy
36 | * modern: sans-serif
37 | * tech: monospace
38 | * bidi: serif
39 | * Font size (as `font-size: #pt`)
40 | * Bold (as ``)
41 | * Italic (as ``)
42 | * Underline (as ``)
43 | * Strikethrough (as ``)
44 | * Superscript (as ``)
45 | * Subscript (as ``)
46 | * Foreground color (as `color: rgb(#,#,#)`)
47 | * Background color (as `color: rgb(#,#,#)`)
48 | * Paragraph first-line indents (as `text-indent: #pt`)
49 | * Idented regions (as `padding-left: #pt`)
50 | * Text alignment: left, right, center, justify (as `text-align:`)
51 |
52 | ## rtfToHTML([opts], cb) → WritableStream
53 |
54 | * opts - Optional options to pass to the HTML generator. See the section on [Options](#options) for details.
55 | * cb - A callback accepting `(err, html)`, see the section on the [Callback](#callback) for details.
56 |
57 | Returns a WritableStream that you can pipe into.
58 |
59 | ## rtfToHTML.fromStream(stream[, opts], cb)
60 |
61 | * stream - A readable stream that should contain RTF.
62 | * opts - Optional options to pass to the HTML generator. See the section on [Options](#options) for details.
63 | * cb - A callback accepting `(err, html)`, see the section on the [Callback](#callback) for details.
64 |
65 | ## rtfToHTML.fromString(string[, opts], cb)
66 |
67 | * string - A string containing RTF.
68 | * opts - Optional options to pass to the HTML generator. See the section on [Options](#options) for details.
69 | * cb - A callback accepting `(err, html)`, see the section on the [Callback](#callback) for details.
70 |
71 | ## Callback
72 |
73 |
74 | rtfToHTML returns HTML produced from RTF using a standard Node.js style
75 | callback, which should accept the following arguments: `(err, html)`.
76 |
77 | If we encounter an error in parsing then it will be set in `err`. Otherwise
78 | the resulting HTML will be in `html`.
79 |
80 | ## Options
81 |
82 |
83 | Options are always optional. You can configure how HTML is generated with the following:
84 |
85 | * paraBreaks - (Defaults to `\n\n`) Inserted between resulting paragraphs.
86 | * paraTag - (Defaults to `p`) The tagname to use for paragraphs.
87 | * template - A function that is used to generate the final HTML document, defaults to:
88 | ```js
89 | function outputTemplate (doc, defaults, content) {
90 | return `
91 |
92 |
93 |
94 |
104 |
105 |
106 | ${content.replace(/\n/, '\n ')}
107 |
108 | `
109 | }
110 | ```
111 |
112 | You can also configure some of the starting (default) state of the output formatting with:
113 |
114 | * disableFonts - Defaults to `true`. If you set this to `false` then we'll output font change information when we encounter it. This is
115 | a bit broken due to our not supporting styles.
116 | * fontSize - Defaults to the document-wide declared font size, or if that's missing, `24`.
117 | * bold - Defaults to `false`
118 | * italic - Defaults to `false`
119 | * underline - Defaults to `false`
120 | * strikethrough - Defaults to `false`
121 | * foreground - Defaults to `{red: 0, blue: 0, green: 0}`
122 | * background - Defaults to `{red: 255, blue: 255, green: 255}`
123 | * firstLineIndent - Defaults to the document-wide value, or if that's missing, `0`. This is how far to indent the first line of new paragraphs.
124 | * indent: Defaults to `0`
125 | * align: Defaults to `left`.
126 | * valign: Defaults to `normal`
127 |
128 | ## const rtfToHTML = require('rtf-to-html/rtf-to-html.js')
129 | ## rtfToHTML(doc[, opts]) → HTML
130 |
131 | This is internally how the other interfaces are implemented. Unlike the
132 | other interfaces, this one is synchronous.
133 |
134 | * doc - A parsed RTF document as produced by the `rtf-parser` library.
135 | * opts - Optional options, see the section on [Options](#options) for details.
136 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const parse = require('rtf-parser')
3 | const rtfToHTML = require('./rtf-to-html.js')
4 |
5 | module.exports = asStream
6 | module.exports.fromStream = fromStream
7 | module.exports.fromString = fromString
8 |
9 | function asStream (opts, cb) {
10 | if (arguments.length === 1) {
11 | cb = opts
12 | opts = null
13 | }
14 | return parse(htmlifyresult(opts, cb))
15 | }
16 |
17 | function fromStream (stream, opts, cb) {
18 | if (arguments.length === 2) {
19 | cb = opts
20 | opts = null
21 | }
22 | return parse.stream(stream, htmlifyresult(opts, cb))
23 | }
24 |
25 | function fromString (string, opts, cb) {
26 | if (arguments.length === 2) {
27 | cb = opts
28 | opts = null
29 | }
30 | return parse.string(string, htmlifyresult(opts, cb))
31 | }
32 |
33 | function htmlifyresult (opts, cb) {
34 | return (err, doc) => {
35 | if (err) return cb(err)
36 | try {
37 | return cb(null, rtfToHTML(doc, opts))
38 | } catch (ex) {
39 | return cb(ex)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@iarna/rtf-to-html",
3 | "version": "1.1.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "echo \"no test specified\"",
7 | "prerelease": "npm t",
8 | "release": "standard-version -s",
9 | "postrelease": "npm publish && git push --follow-tags",
10 | "pretest": "standard",
11 | "update-coc": "weallbehave -o . && git add CODE_OF_CONDUCT.md && git commit -m 'docs(coc): updated CODE_OF_CONDUCT.md'",
12 | "update-contrib": "weallcontribute -o . && git add CONTRIBUTING.md && git commit -m 'docs(contributing): updated CONTRIBUTING.md'"
13 | },
14 | "keywords": [],
15 | "author": "Rebecca Turner (http://re-becca.org/)",
16 | "license": "ISC",
17 | "dependencies": {
18 | "rtf-parser": "^1.0.4"
19 | },
20 | "devDependencies": {
21 | "weallbehave": "*",
22 | "weallcontribute": "*",
23 | "standard": "*",
24 | "standard-version": "*",
25 | "tap": "*"
26 | },
27 | "description": "Convert RTF to HTML in pure JavaScript.",
28 | "files": [
29 | "index.js",
30 | "rtf-to-html.js"
31 | ],
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/iarna/rtf-to-html.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/iarna/rtf-to-html/issues"
38 | },
39 | "homepage": "https://npmjs.com/package/@iarna/rtf-to-html"
40 | }
41 |
--------------------------------------------------------------------------------
/rtf-to-html.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = rtfToHTML
3 |
4 | function outputTemplate (doc, defaults, content) {
5 | return `
6 |
7 |
8 |
9 |
19 |
20 |
21 | ${content.replace(/\n/, '\n ')}
22 |
23 |
24 | `
25 | }
26 |
27 | function rtfToHTML (doc, options) {
28 | const defaults = Object.assign({
29 | font: doc.style.font || {name: 'Times', family: 'roman'},
30 | fontSize: doc.style.fontSize || 24,
31 | bold: false,
32 | italic: false,
33 | underline: false,
34 | strikethrough: false,
35 | foreground: {red: 0, blue: 0, green: 0},
36 | background: {red: 255, blue: 255, green: 255},
37 | firstLineIndent: doc.style.firstLineIndent || 0,
38 | indent: 0,
39 | align: 'left',
40 | valign: 'normal',
41 |
42 | paraBreaks: '\n\n',
43 | paraTag: 'p',
44 | template: outputTemplate
45 | }, options || {})
46 | const content = doc.content
47 | .map(para => renderPara(para.content ? para : {content: [para], style: {}}, defaults))
48 | .filter(html => html != null)
49 | .join(defaults.paraBreaks)
50 | return defaults.template(doc, defaults, content)
51 | }
52 |
53 | function font (ft) {
54 | const name = ft.name.replace(/-\w+$/, '')
55 | const family = genericFontMap[ft.family]
56 | if (name === 'ZapfDingbatsITC') return ''
57 | return 'font-family: ' + name + (family ? `, ${family}` : '')
58 | }
59 |
60 | const genericFontMap = {
61 | roman: 'serif',
62 | swiss: 'sans-serif',
63 | script: 'cursive',
64 | decor: 'fantasy',
65 | modern: 'sans-serif',
66 | tech: 'monospace',
67 | bidi: 'serif'
68 | }
69 |
70 | function colorEq (aa, bb) {
71 | return aa.red === bb.red && aa.blue === bb.blue && aa.green === bb.green
72 | }
73 |
74 | function CSS (chunk, defaults) {
75 | let css = ''
76 | if (chunk.style.foreground != null && !colorEq(chunk.style.foreground, defaults.foreground)) {
77 | css += `color: rgb(${chunk.style.foreground.red}, ${chunk.style.foreground.green}, ${chunk.style.foreground.blue});`
78 | }
79 | if (chunk.style.background != null && !colorEq(chunk.style.background, defaults.background)) {
80 | css += `background-color: rgb(${chunk.style.background.red}, ${chunk.style.background.green}, ${chunk.style.background.blue});`
81 | }
82 | if (chunk.style.firstLineIndent != null && chunk.style.firstLineIndent > 0 && chunk.style.firstLineIndent !== defaults.firstLineIndent) {
83 | css += `text-indent: ${chunk.style.firstLineIndent / 20}pt;`
84 | }
85 | if (chunk.style.indent != null && chunk.style.indent !== defaults.indent) {
86 | css += `padding-left: ${chunk.style.indent / 20}pt;`
87 | }
88 | if (chunk.style.align != null && chunk.style.align !== defaults.align) {
89 | css += `text-align: ${chunk.style.align};`
90 | }
91 | if (chunk.style.fontSize != null && chunk.style.fontSize !== defaults.fontSize) {
92 | css += `font-size: ${chunk.style.fontSize / 2}pt;`
93 | }
94 | if (!defaults.disableFonts && chunk.style.font != null && chunk.style.font.name !== defaults.font.name) {
95 | css += font(chunk.style.font)
96 | }
97 | return css
98 | }
99 |
100 | function styleTags (chunk, defaults) {
101 | let open = ''
102 | let close = ''
103 | if (chunk.style.italic != null && chunk.style.italic !== defaults.italic) {
104 | open += ''
105 | close = '' + close
106 | }
107 | if (chunk.style.bold != null && chunk.style.bold !== defaults.bold) {
108 | open += ''
109 | close = '' + close
110 | }
111 | if (chunk.style.strikethrough != null && chunk.style.strikethrough !== defaults.strikethrough) {
112 | open += ''
113 | close = '' + close
114 | }
115 | if (chunk.style.underline != null && chunk.style.underline !== defaults.underline) {
116 | open += ''
117 | close = '' + close
118 | }
119 | if (chunk.style.valign != null && chunk.style.valign !== defaults.valign) {
120 | if (chunk.style.valign === 'super') {
121 | open += ''
122 | close = '' + close
123 | } else if (chunk.style.valign === 'sub') {
124 | open += ''
125 | close = '' + close
126 | }
127 | }
128 | return {open, close}
129 | }
130 |
131 | function renderPara (para, defaults) {
132 | if (!para.content || para.content.length === 0) return
133 | const style = CSS(para, defaults)
134 | const tags = styleTags(para, defaults)
135 | const pdefaults = Object.assign({}, defaults)
136 | for (let item of Object.keys(para.style)) {
137 | if (para.style[item] != null) pdefaults[item] = para.style[item]
138 | }
139 | const paraTag = defaults.paraTag
140 | return `<${paraTag}${style ? ' style="' + style + '"' : ''}>${tags.open}${para.content.map(span => renderSpan(span, pdefaults)).join('')}${tags.close}${paraTag}>`
141 | }
142 |
143 | function renderSpan (span, defaults) {
144 | const style = CSS(span, defaults)
145 | const tags = styleTags(span, defaults)
146 | const value = `${tags.open}${span.value}${tags.close}`
147 | if (style) {
148 | return `${value}`
149 | } else {
150 | return value
151 | }
152 | }
153 |
--------------------------------------------------------------------------------