Permalink
Browse files

Support for in-line and external images.

  • Loading branch information...
1 parent feb181a commit 6d3a263f568dcaf279bbfd19883602db8e7f8ed2 @evanmiller committed Jun 14, 2011
View
@@ -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) ->
@@ -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".
+
View
@@ -10,5 +10,6 @@
ansi_code_page = undefined,
table = false,
unicode_size = 1,
- double_quote_count = 0
+ double_quote_count = 0,
+ image_fun
}).
@@ -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));
@@ -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);
@@ -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).
@@ -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]"];
@@ -22,6 +22,8 @@ Terminals
open_url_equals
url_value
close_url
+ open_img
+ close_img
open_quote
close_quote
open_code
@@ -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'}.
@@ -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) ->
@@ -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>"];
@@ -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) ->
@@ -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);
@@ -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 "];
@@ -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).
@@ -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));
@@ -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).
@@ -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), "*"];
@@ -25,7 +25,8 @@ Terminals
text
header_cell_start
cell_delimiter
- hyperlink
+ url
+ image
double_quote
caret
tilde.
@@ -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'}.
@@ -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))};
@@ -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) ->

0 comments on commit 6d3a263

Please sign in to comment.