Skip to content

Commit f2e7506

Browse files
committed
initial
0 parents  commit f2e7506

File tree

5 files changed

+243
-0
lines changed

5 files changed

+243
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
*~
3+
.#*

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# @iarna/rtf-to-html
2+
3+
Convert RTF to HTML in pure JavaScript.
4+
5+
```
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+
// <p><strong>hi there</strong></p>
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 `<p>` 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 `<strong>`)
41+
* Italic (as `<em>`)
42+
* Underline (as `<u>`)
43+
* Strikethrough (as `<s>`)
44+
* Superscript (as `<sup>`)
45+
* Subscript (as `<sub>`)
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:`)

index.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 (cb) {
10+
return parse(htmlifyresult(cb))
11+
}
12+
13+
function fromStream (stream, cb) {
14+
return parse.stream(stream, htmlifyresult(cb))
15+
}
16+
17+
function fromString (string, cb) {
18+
return parse.string(string, htmlifyresult(cb))
19+
}
20+
21+
function htmlifyresult (cb) {
22+
return (err, doc) => {
23+
if (err) return cb(err)
24+
try {
25+
return cb(null, rtfToHTML(doc))
26+
} catch (ex) {
27+
return cb(ex)
28+
}
29+
}
30+
}
31+

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@iarna/rtf-to-html",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1"
7+
},
8+
"keywords": [],
9+
"author": "Rebecca Turner <[email protected]> (http://re-becca.org/)",
10+
"license": "ISC",
11+
"dependencies": {
12+
"rtf-parser": "^1.0.1"
13+
},
14+
"devDependencies": {},
15+
"description": "Convert RTF to HTML in pure JavaScript.",
16+
"files": [
17+
"index.js",
18+
"rtf-to-html.js"
19+
],
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/iarna/rtf-to-html.git"
23+
},
24+
"bugs": {
25+
"url": "https://github.com/iarna/rtf-to-html/issues"
26+
},
27+
"homepage": "https://npmjs.com/package/@iarna/rtf-to-html"
28+
}

rtf-to-html.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict'
2+
module.exports = rtfToHTML
3+
4+
function rtfToHTML (doc) {
5+
const defaults = {
6+
font: doc.content[0].style.font || {name: 'Times', family: 'roman'},
7+
fontSize: doc.content[0].style.fontSize || 24,
8+
bold: false,
9+
italic: false,
10+
underline: false,
11+
strikethrough: false,
12+
foreground: {red: 0, blue: 0, green: 0},
13+
background: {red: 255, blue: 255, green: 255},
14+
firstLineIndent: doc.content[0].style.firstLineIndent || 0,
15+
indent: 0,
16+
align: 'left',
17+
valign: 'normal'
18+
}
19+
const content = doc.content.map(para => renderPara(para, defaults)).filter(html => html != null).join('\n\n')
20+
return `<!DOCTYPE html>
21+
<html>
22+
<head>
23+
<meta charset="UTF-8">
24+
<style>
25+
body {
26+
margin-left: ${doc.marginLeft / 20}pt;
27+
margin-right: ${doc.marginRight / 20}pt;
28+
margin-top: ${doc.marginTop / 20}pt;
29+
margin-bottom: ${doc.marginBottom /20}pt;
30+
font-family: ${defaults.font.name.replace(/-\w+$/,'')}, ${genericFontMap[defaults.font.family]};
31+
text-indent: ${defaults.firstLineIndent / 20}pt;
32+
}
33+
</style>
34+
</head>
35+
<body>
36+
${content}
37+
</body>
38+
</html>
39+
`
40+
}
41+
42+
const genericFontMap = {
43+
roman: 'serif',
44+
swiss: 'sans-serif',
45+
script: 'cursive',
46+
decor: 'fantasy',
47+
modern: 'sans-serif',
48+
tech: 'monospace',
49+
bidi: 'serif'
50+
}
51+
52+
function colorEq (aa, bb) {
53+
if (!aa && !bb) return true
54+
if (!bb) return false
55+
return aa.red === bb.red && aa.blue === bb.blue && aa.green === bb.green
56+
}
57+
58+
function CSS (chunk, defaults) {
59+
let css = ''
60+
if (chunk.style.foreground != null && !colorEq(chunk.style.foreground, defaults.foreground)) {
61+
css += `color: rgb(${chunk.style.foreground.red}, ${chunk.style.foreground.green}, ${chunk.style.foreground.blue});`
62+
}
63+
if (chunk.style.background != null && !colorEq(chunk.style.background, defaults.background)) {
64+
css += `background-color: rgb(${chunk.style.background.red}, ${chunk.style.background.green}, ${chunk.style.background.blue});`
65+
}
66+
if (chunk.style.firstLineIndent != null && chunk.style.firstLineIndent > 0 && chunk.style.firstLineIndent !== defaults.firstLineIndent) {
67+
css += `text-indent: ${chunk.style.firstLineIndent / 20}pt;`
68+
}
69+
if (chunk.style.indent != null && chunk.style.indent !== defaults.indent) {
70+
css += `padding-left: ${chunk.style.indent / 20}pt;`
71+
}
72+
if (chunk.style.align != null && chunk.style.align !== defaults.align) {
73+
css += `text-align: ${chunk.style.align};`
74+
}
75+
if (chunk.style.fontSize != null && chunk.style.fontSize !== defaults.fontSize) {
76+
css += `font-size: ${chunk.style.fontSize / 2}pt;`
77+
}
78+
if (chunk.style.font != null && chunk.style.font.name !== defaults.font.name) {
79+
css += `font-family: ${chunk.style.font.name.replace(/-\w+$/,'')}, ${genericFontMap[chunk.style.font.family]};`
80+
}
81+
return css
82+
}
83+
84+
function styleTags (chunk, defaults) {
85+
let open = ''
86+
let close = ''
87+
if (chunk.style.italic != null && chunk.style.italic !== defaults.italic) {
88+
open += '<em>'
89+
close = '</em>' + close
90+
}
91+
if (chunk.style.bold != null && chunk.style.bold !== defaults.bold) {
92+
open += '<strong>'
93+
close = '</strong>' + close
94+
}
95+
if (chunk.style.strikethrough != null && chunk.style.strikethrough !== defaults.strikethrough) {
96+
open += '<s>'
97+
close = '</s>' + close
98+
}
99+
if (chunk.style.underline != null && chunk.style.underline !== defaults.underline) {
100+
open += '<u>'
101+
close = '</u>' + close
102+
}
103+
if (chunk.style.valign != null && chunk.style.valign !== defaults.valign) {
104+
if (chunk.style.valign === 'super') {
105+
open += '<sup>'
106+
close = '</sup>' + close
107+
} else if (chunk.style.valign === 'sub') {
108+
open += '<sup>'
109+
close = '</sup>' + close
110+
}
111+
}
112+
return {open, close}
113+
}
114+
115+
function renderPara (para, defaults) {
116+
if (para.content.length === 0) return
117+
const style = CSS(para, defaults)
118+
const tags = styleTags(para, defaults)
119+
return `<p${style ? ' style="' + style + '"' : ''}>${tags.open}${para.content.map(span => renderSpan(span, Object.assign({}, defaults, para.style))).join('')}${tags.close}</p>`
120+
}
121+
122+
function renderSpan (span, defaults) {
123+
const style = CSS(span, defaults)
124+
const tags = styleTags(span, defaults)
125+
const value = `${tags.open}${span.value}${tags.close}`
126+
if (style) {
127+
return `<span style="${style}">${value}</span>`
128+
} else {
129+
return value
130+
}
131+
}

0 commit comments

Comments
 (0)