Skip to content
This repository has been archived by the owner on Dec 17, 2022. It is now read-only.

Commit

Permalink
Support for in-line and external images.
Browse files Browse the repository at this point in the history
  • Loading branch information
evanmiller committed Jun 14, 2011
1 parent feb181a commit 6d3a263
Show file tree
Hide file tree
Showing 14 changed files with 110 additions and 42 deletions.
29 changes: 21 additions & 8 deletions src/jerome.erl
Expand Up @@ -6,21 +6,26 @@

% Jerome - a rich-text reader/writer

parse(Path, Format) when is_list(Path) ->
parse(Path, Format) ->
parse(Path, Format, fun(Img) -> {ok, Img} end).

parse(Path, Format, ImageFun) when is_list(Path) ->
{ok, Binary} = file:read_file(Path),
parse(Binary, Format);
parse(Binary, Format, ImageFun);

parse(Binary, Format) when is_binary(Binary) ->
parse(Binary, Format, ImageFun) when is_binary(Binary) ->
case Format of
rtf -> jerome_rtf_consumer:consume(Binary)
bbcode -> jerome_bbcode_consumer:consume(Binary, ImageFun);
rtf -> jerome_rtf_consumer:consume(Binary, ImageFun);
textile -> jerome_textile_consumer:consume(Binary, ImageFun)
end.

generate(Ast, Format) ->
case Format of
html ->
jerome_html_generator:generate(Ast);
rtf ->
jerome_rtf_generator:generate(Ast)
bbcode -> jerome_bbcode_generator:generate(Ast);
html -> jerome_html_generator:generate(Ast);
rtf -> jerome_rtf_generator:generate(Ast);
textile -> jerome_textile_generator:generate(Ast)
end.

consolidate(List) ->
Expand All @@ -47,3 +52,11 @@ text_properties(#jerome_ctx{ subscript = true } = Ctx) ->
[subscript] ++ text_properties(Ctx#jerome_ctx{ subscript = false });
text_properties(_) -> [].

image_mime_type(<<137, $P, $N, $G,$\r, $\n, 26, $\n, _/binary>>) -> "image/png";
image_mime_type(<<16#FF, 16#D8, 16#FF, 16#E0, _/binary>>) -> "image/jpeg";
image_mime_type(<<16#FF, 16#D8, 16#FF, 16#E1, _/binary>>) -> "image/jpeg";
image_mime_type(<<$G, $I, $F, $8, $7, $a>>) -> "image/gif";
image_mime_type(<<$G, $I, $F, $8, $9, $a>>) -> "image/gif";
image_mime_type(<<(16#4949):16, (42):16/little, _/binary>>) -> "image/tiff";
image_mime_type(<<(16#4D4D):16, (42):16/big, _/binary>>) -> "image/tiff".

3 changes: 2 additions & 1 deletion src/jerome.hrl
Expand Up @@ -10,5 +10,6 @@
ansi_code_page = undefined,
table = false,
unicode_size = 1,
double_quote_count = 0
double_quote_count = 0,
image_fun
}).
19 changes: 12 additions & 7 deletions src/jerome_bbcode_consumer.erl
@@ -1,16 +1,16 @@
-module(jerome_bbcode_consumer).

-export([consume/1]).
-export([consume/2]).

-include("jerome.hrl").

consume(Binary) when is_binary(Binary) ->
{ok, Tokens} = jerome_bbcode_scanner:scan(binary_to_list(Binary)),
consume(Binary, ImageFun) when is_binary(Binary), is_function(ImageFun) ->
{ok, Tokens} = jerome_bbcode_scanner:scan(unicode:characters_to_list(Binary)),
{ok, ParseTree} = jerome_bbcode_parser:parse(Tokens),
process_tree(ParseTree).
process_tree(ParseTree, ImageFun).

process_tree(Tree) ->
process_tree(Tree, [], #jerome_ctx{}).
process_tree(Tree, ImageFun) ->
process_tree(Tree, [], #jerome_ctx{ image_fun = ImageFun }).

process_tree([], Acc, _) ->
jerome:consolidate(lists:reverse(Acc));
Expand All @@ -35,6 +35,9 @@ process_tree([{hyperlink, {text, _, Value} = Token}|Rest], Acc, Context) ->
process_tree([{hyperlink, {text, _, Value}, Elements}|Rest], Acc, Context) ->
Ast = process_tree(Elements, [], Context#jerome_ctx{ hyperlink = Value }),
process_tree(Rest, lists:reverse(Ast, Acc), Context);
process_tree([{image, {text, _, Value}}|Rest], Acc, Context) ->
{ok, Image} = (Context#jerome_ctx.image_fun)(Value),
process_tree(Rest, [{image, Image}|Acc], Context);
process_tree([{quote, Elements}|Rest], Acc, Context) ->
Ast = process_tree(Elements, [], Context),
process_tree(Rest, [{blockquote, Ast}|Acc], Context);
Expand All @@ -58,5 +61,7 @@ process_tree([{table_cell, Elements}|Rest], Acc, Context) ->
process_tree(Rest, [{table_cell, Ast}|Acc], Context);
process_tree([{text, _, Text}|Rest], Acc, Context) ->
process_tree(Rest, [{text, Text, jerome:text_properties(Context)}|Acc], Context);
process_tree([{newline, _}, {newline, _}|Rest], Acc, Context) ->
process_tree(Rest, [{paragraph, left}, {paragraph, left}|Acc], Context);
process_tree([{newline, _}|Rest], Acc, Context) ->
process_tree(Rest, [{paragraph, left}|Acc], Context).
process_tree(Rest, Acc, Context).
5 changes: 4 additions & 1 deletion src/jerome_bbcode_generator.erl
Expand Up @@ -29,7 +29,10 @@ generate([{blockcode, Ast}|Rest], Acc) ->
generate([{preformatted, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["[code]", generate(Ast, []), "[/pre]"], Acc));
generate([{heading, Level, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["[h"++integer_to_list(Level)++"]", generate(Ast, []), "[/h"++integer_to_list(Level)++"]"], Acc)).
generate(Rest, lists:reverse(["[h", integer_to_list(Level), "]", generate(Ast, []),
"[/h", integer_to_list(Level), "]"], Acc));
generate([{image, ImageURL}|Rest], Acc) when is_list(ImageURL) ->
generate(Rest, lists:reverse(["[img]", ImageURL, "[/img]"], Acc)).

write_attributed_text(Text, [bold|Rest]) ->
["[b]", write_attributed_text(Text, Rest), "[/b]"];
Expand Down
3 changes: 3 additions & 0 deletions src/jerome_bbcode_parser.yrl
Expand Up @@ -22,6 +22,8 @@ Terminals
open_url_equals
url_value
close_url
open_img
close_img
open_quote
close_quote
open_code
Expand Down Expand Up @@ -52,6 +54,7 @@ Elements -> open_superscript Elements close_superscript : {superscript, '$2'}.
Elements -> open_subscript Elements close_subscript : {subscript, '$2'}.
Elements -> open_url text close_url : {hyperlink, '$2'}.
Elements -> open_url_equals url_value Elements close_url : {hyperlink, '$2', '$3'}.
Elements -> open_img text close_img : {image, '$2'}.
Elements -> open_quote Elements close_quote : {quote, '$2'}.
Elements -> open_code Elements close_code : {code, '$2'}.
Elements -> open_list ListItems close_list : {list, '$2'}.
Expand Down
4 changes: 4 additions & 0 deletions src/jerome_bbcode_scanner.erl
Expand Up @@ -48,6 +48,10 @@ scan([H|T], [{url_value, UPos, Value}|Scanned], {Row, Column}, in_url) ->
scan(T, [{url_value, UPos, [H|Value]}|Scanned], {Row, Column + 1}, in_url);
scan([H|T], Scanned, {Row, Column} = Pos, in_url) ->
scan(T, [{url_value, Pos, [H]}|Scanned], {Row, Column + 1}, in_url);
scan("[img]"++T, Scanned, {Row, Column} = Pos, text) ->
scan(T, [{open_img, Pos}|Scanned], {Row, Column + length("[img]")}, text);
scan("[/img]"++T, Scanned, {Row, Column} = Pos, text) ->
scan(T, [{close_img, Pos}|Scanned], {Row, Column + length("[/img]")}, text);
scan("[quote]"++T, Scanned, {Row, Column} = Pos, text) ->
scan(T, [{open_quote, Pos}|Scanned], {Row, Column + length("[quote]")}, text);
scan("[/quote]"++T, Scanned, {Row, Column} = Pos, text) ->
Expand Down
8 changes: 7 additions & 1 deletion src/jerome_html_generator.erl
Expand Up @@ -29,7 +29,13 @@ generate([{blockcode, Ast}|Rest], Acc) -> % TODO
generate([{preformatted, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["<pre>", generate(Ast, []), "</pre>"], Acc));
generate([{heading, Level, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["<h"++integer_to_list(Level)++">", generate(Ast, []), "</h"++integer_to_list(Level)++">"], Acc)).
generate(Rest, lists:reverse(["<h", integer_to_list(Level), ">", generate(Ast, []),
"</h", integer_to_list(Level), ">"], Acc));
generate([{image, ImageURL}|Rest], Acc) when is_list(ImageURL) ->
generate(Rest, lists:reverse(["<img src=\"", ImageURL, "\" />"], Acc));
generate([{image, ImageBinary}|Rest], Acc) when is_binary(ImageBinary) ->
generate(Rest, lists:reverse(["<img src=\"data:", jerome:image_mime_type(ImageBinary),
";base64,", base64:encode_to_string(ImageBinary), "\" />"], Acc)).

write_attributed_text(Text, [bold|Rest]) ->
["<strong>", write_attributed_text(Text, Rest), "</strong>"];
Expand Down
16 changes: 10 additions & 6 deletions src/jerome_rtf_consumer.erl
Expand Up @@ -2,19 +2,19 @@

-include("jerome.hrl").

-export([consume/1]).
-export([consume/2]).

recognize_word("fldinst") -> true;
recognize_word(_) -> false.

consume(Binary) when is_binary(Binary) ->
{ok, Tokens} = jerome_rtf_scanner:scan(binary_to_list(Binary)),
consume(Binary, ImageFun) when is_binary(Binary) ->
{ok, Tokens} = jerome_rtf_scanner:scan(unicode:characters_to_list(Binary)),
{ok, ParseTree} = jerome_rtf_parser:parse(Tokens),
PrunedTree = prune(ParseTree),
process_tree(PrunedTree).
process_tree(PrunedTree, ImageFun).

process_tree(PrunedTree) ->
{Ast, _Context} = process_tree(PrunedTree, [], #jerome_ctx{}),
process_tree(PrunedTree, ImageFun) ->
{Ast, _Context} = process_tree(PrunedTree, [], #jerome_ctx{ image_fun = ImageFun }),
Ast.

process_tree([], Acc, Context) ->
Expand Down Expand Up @@ -57,6 +57,10 @@ process_tree([{control_word, _, "pard"}|Rest], Acc, Context) ->
process_tree(Rest, Acc, Context#jerome_ctx{ paragraph_alignment = left, table = false });
process_tree([{control_word, _, "intbl"}|Rest], Acc, Context) ->
process_tree(Rest, Acc, Context#jerome_ctx{ table = true });
process_tree([{group, [{control_word, _, "NeXTGraphic"},
{text, _, Graphic}|_]}|Rest], Acc, Context) ->
{ok, Image} = (Context#jerome_ctx.image_fun)(Graphic),
process_tree(Rest, [{image, Image}|Acc], Context);
process_tree([{table_row, Tree}|Rest], [{table, Rows}|Acc], Context) ->
{Ast, Context1} = process_tree(Tree, [], Context),
process_tree(Rest, [{table, Rows ++ [{table_row, Ast}]}|Acc], Context1);
Expand Down
11 changes: 10 additions & 1 deletion src/jerome_rtf_generator.erl
Expand Up @@ -23,7 +23,16 @@ generate([{list, ListItems}|Rest], Acc) ->
generate([{list_item, ListItem}|Rest], Acc) ->
generate(Rest, lists:reverse(["{\\listtext\\uc0\\u9642}", generate(ListItem, [])], Acc));
generate([{paragraph, _}|Rest], Acc) ->
generate(Rest, ["\\\n"|Acc]).
generate(Rest, ["\\\n"|Acc]);
generate([{image, ImageBinary}|Rest], Acc) when is_binary(ImageBinary) ->
{PictType, Width, Height} = image_info(ImageBinary),
generate(Rest, lists:reverse(["{\\*\\shppict {\\pict ",
"\\picw", integer_to_list(Width), "\\pich", integer_to_list(Height),
PictType, "\\bin", integer_to_list(byte_size(ImageBinary)), " ",
ImageBinary, "}}"], Acc)).

image_info(<<137, $P, $N, $G, $\r, $\n, 26, $\n, _Length:32, $I, $H, $D, $R, Width:32, Height:32>>) ->
{"\\pngblip", Width, Height}.

write_attributed_text(Text, [bold|Rest]) ->
["\\b ", write_attributed_text(Text, Rest), "\\b0 "];
Expand Down
7 changes: 5 additions & 2 deletions src/jerome_rtf_scanner.erl
Expand Up @@ -121,8 +121,11 @@ scan([_H|T], Scanned, {Row, Column}, State) when State =:= in_word; State =:= in
scan([H|T], [{text, TPos, Text}|Scanned], {Row, Column}, in_text) ->
scan(T, [{text, TPos, [H|Text]}|Scanned], {Row, Column + 1}, in_text);
scan([H|T], Scanned, {Row, Column} = Pos, in_text) ->
scan(T, [{text, Pos, [H]}|Scanned], {Row, Column+1}, in_text).
scan([H|T], Scanned, {Row, Column} = Pos, in_text) when H =< 127->
scan(T, [{text, Pos, [H]}|Scanned], {Row, Column+1}, in_text);
scan([_H|T], Scanned, {Row, Column}, in_text) -> % RTFD inserts some useless Unicode
scan(T, Scanned, {Row, Column+1}, in_text).
Expand Down
18 changes: 11 additions & 7 deletions src/jerome_textile_consumer.erl
Expand Up @@ -2,15 +2,15 @@

-include("jerome.hrl").

-export([consume/1]).
-export([consume/2]).

consume(Binary) when is_binary(Binary) ->
consume(Binary, ImageFun) when is_binary(Binary), is_function(ImageFun) ->
{ok, Tokens} = jerome_textile_scanner:scan(binary_to_list(Binary)),
{ok, ParseTree} = jerome_textile_parser:parse(Tokens),
process_tree(ParseTree).
process_tree(ParseTree, ImageFun).

process_tree(Tree) ->
process_tree(Tree, [], #jerome_ctx{}).
process_tree(Tree, ImageFun) ->
process_tree(Tree, [], #jerome_ctx{ image_fun = ImageFun }).

process_tree([], Acc, _) ->
jerome:consolidate(lists:reverse(Acc));
Expand Down Expand Up @@ -67,10 +67,14 @@ process_tree([{punctuation, _, " x "}|Rest], Acc, Context) ->
process_tree(Rest, [{text, [$\ , 16#00D7, $\ ], Props}|Acc], Context);
process_tree([{newline, _}|Rest], Acc, _Context) ->
process_tree(Rest, [{paragraph, left}|Acc], #jerome_ctx{});
process_tree([{hyperlink, _, Elements, {hyperlink, _, Link}}|Rest], Acc, Context) ->
process_tree([{hyperlink, _, Elements, {url, _, Link}}|Rest], Acc, Context) ->
Ast = process_tree(Elements, [], Context#jerome_ctx{ hyperlink = Link }),
process_tree(Rest, lists:reverse(Ast, Acc), Context);
process_tree([{double_quote, _, _}|Rest], Acc, #jerome_ctx{ double_quote_count = Count } = Context) ->
process_tree(Rest, [{text, [16#201C + (Count rem 2)], jerome:text_properties(Context)}|Acc], Context#jerome_ctx{ double_quote_count = Count + 1 });
process_tree(Rest, [{text, [16#201C + (Count rem 2)], jerome:text_properties(Context)}|Acc],
Context#jerome_ctx{ double_quote_count = Count + 1 });
process_tree([{image, _, ImageURL}|Rest], Acc, #jerome_ctx{ image_fun = ImageFun } = Context) ->
{ok, ImageResult} = ImageFun(ImageURL),
process_tree(Rest, [{image, ImageResult}|Acc], Context);
process_tree([{text, _, Text}|Rest], Acc, Context) ->
process_tree(Rest, [{text, Text, jerome:text_properties(Context)}|Acc], Context).
4 changes: 3 additions & 1 deletion src/jerome_textile_generator.erl
Expand Up @@ -28,7 +28,9 @@ generate([{blockcode, Ast}|Rest], Acc) ->
generate([{preformatted, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["pre. ", generate(Ast, [])], Acc));
generate([{heading, Level, Ast}|Rest], Acc) ->
generate(Rest, lists:reverse(["h"++integer_to_list(Level)++". ", generate(Ast, [])], Acc)).
generate(Rest, lists:reverse(["h"++integer_to_list(Level)++". ", generate(Ast, [])], Acc));
generate([{image, ImageURL}|Rest], Acc) when is_list(ImageURL) ->
generate(Rest, lists:reverse(["!", ImageURL, "!"], Acc)).

write_attributed_text(Text, [bold|Rest]) ->
["*", write_attributed_text(Text, Rest), "*"];
Expand Down
6 changes: 4 additions & 2 deletions src/jerome_textile_parser.yrl
Expand Up @@ -25,7 +25,8 @@ Terminals
text
header_cell_start
cell_delimiter
hyperlink
url
image
double_quote
caret
tilde.
Expand Down Expand Up @@ -63,11 +64,12 @@ NonEmptyTextElements -> NonEmptyTextElements TextElement : '$1' ++ ['$2'].
TextElement -> text : '$1'.
TextElement -> punctuation : '$1'.
TextElement -> double_quote : '$1'.
TextElement -> image : '$1'.
TextElement -> single_star NonEmptyTextElements single_star : {strong, '$2'}.
TextElement -> double_star NonEmptyTextElements double_star : {bold, '$2'}.
TextElement -> single_underscore NonEmptyTextElements single_underscore : {em, '$2'}.
TextElement -> double_underscore NonEmptyTextElements double_underscore : {italic, '$2'}.
TextElement -> double_quote NonEmptyLinkElements double_quote hyperlink : {hyperlink, '$2', '$4'}.
TextElement -> double_quote NonEmptyLinkElements double_quote url : {hyperlink, '$2', '$4'}.
TextElement -> caret NonEmptyTextElements caret : {superscript, '$2'}.
TextElement -> tilde NonEmptyTextElements tilde : {subscript, '$2'}.

Expand Down
19 changes: 14 additions & 5 deletions src/jerome_textile_scanner.erl
Expand Up @@ -11,8 +11,10 @@ scan([], Scanned, _, _) ->
lists:map(fun
({text, Pos, Text}) ->
{text, Pos, lists:reverse(Text)};
({hyperlink, Pos, Link}) ->
{hyperlink, Pos, lists:reverse(Link)};
({url, Pos, Link}) ->
{url, Pos, lists:reverse(Link)};
({image, Pos, Link}) ->
{image, Pos, lists:reverse(Link)};
(Token) ->
Token
end, Scanned))};
Expand Down Expand Up @@ -64,16 +66,23 @@ scan("|_. "++T, Scanned, {Row, Column} = Pos, _) ->
scan("|"++T, Scanned, {Row, Column} = Pos, _) ->
scan(T, [{cell_delimiter, Pos}|Scanned], {Row, Column + 1}, inline);
scan("\":http://"++T, Scanned, {Row, Column} = Pos, inline) ->
scan(T, [{hyperlink, {Row, Column + 2}, lists:reverse("http://")}, {double_quote, Pos}|Scanned],
scan(T, lists:reverse([{double_quote, Pos}, {url, {Row, Column + 2}, lists:reverse("http://")}], Scanned),
{Row, Column + length("\":http://")}, inlink);
scan("\""++T, Scanned, {Row, Column} = Pos, inline) ->
scan(T, [{double_quote, Pos, [$"]}|Scanned], {Row, Column + 1}, inline);
scan("!http://"++T, Scanned, {Row, Column} = Pos, inline) ->
scan(T, [{image, Pos, lists:reverse("http://")}|Scanned],
{Row, Column + length("!http://")}, inimage);
scan("!"++T, Scanned, {Row, Column}, inimage) ->
scan(T, Scanned, {Row, Column + 1}, inline);
scan([H|T], [{url, IPos, Link}|Scanned], {Row, Column}, inimage) ->
scan(T, [{url, IPos, [H|Link]}|Scanned], {Row, Column + 1}, inimage);
scan(" "++T, Scanned, {Row, Column} = Pos, inlink) ->
scan(T, [{text, Pos, " "}|Scanned], {Row, Column + 1}, inline);
scan(". "++T, Scanned, {Row, Column} = Pos, inlink) ->
scan(T, [{text, Pos, ". "}|Scanned], {Row, Column + 2}, inline);
scan([H|T], [{hyperlink, HPos, Link}|Scanned], {Row, Column}, inlink) ->
scan(T, [{hyperlink, HPos, [H|Link]}|Scanned], {Row, Column}, inlink);
scan([H|T], [{url, HPos, Link}|Scanned], {Row, Column}, inlink) ->
scan(T, [{url, HPos, [H|Link]}|Scanned], {Row, Column}, inlink);
scan([H|T], [{text, TPos, Text}|Scanned], {Row, Column}, inline) ->
scan(T, [{text, TPos, [H|Text]}|Scanned], {Row, Column + 1}, inline);
scan([H|T], Scanned, {Row, Column} = Pos, inline) ->
Expand Down

0 comments on commit 6d3a263

Please sign in to comment.