Skip to content

Instantly share code, notes, and snippets.

@221V
Created August 5, 2019 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 221V/61222a424a811039f719de0411257ab4 to your computer and use it in GitHub Desktop.
Save 221V/61222a424a811039f719de0411257ab4 to your computer and use it in GitHub Desktop.
html-xhtml to groff with xmerl [rough code]
.TH n2o 1 "n2o 4.5.0" "Synrc Research Center" "N2O"
.SH NAME
n2o \- Protocol and Application Server
.SH INTRO
.LP
The
\fIn2o\fR\&defines the way you create, configure and run
arbitrary applications and protocols inside some hosts, into
which N2O can be injected, such as
\fIcowboy\fR\&and
\fIemqttd\fR\&.
Each application can spawn its instance in its way like
web pages spawn WebSocket connections, workflow engines
spawn business processes, and chat applications spawns roster
and chatroom processes. With N2O everything is managed by protocols.
.LP
N2O shipped to work in two modes:
1) inside
\fIn2o_mqtt\fR\&workers;
2) inside cowboy processes, implemented in
\fIn2o_stream\fR\&.
In the first case, the MQTT server used between clients and server workers.
In the second case, no more Erlang processes introduced except clients.
You can create your configuration of N2O processing loop.
.LP
.LP
The N2O itself is an embeddable protocol loop in
\fIn2o_proto\fR\&.
However, besides that, it handles cache and sessions
along with flexible
\fIn2o_pi\fR\&processes with no ownership restriction.
It also introduces AES/CBC—128 pickling and BERT/JSON encoder.
.SH TYPES
.nf
-type formatter() :: binary | json | bert | text | default | atom().
-type response() :: { formatter(), binary() }.
.fi
.nf
#ok { data = [] :: term() }.
#error { data = [] :: term() }.
.fi
.nf
#reply { resp = [] :: [] | response(),
req = [] :: [] | term(),
state = [] :: [] | term() }.
#unknown { data = [] :: [] | binary(),
req = [] :: [] | term(),
state = [] :: [] | term() }.
.fi
.nf
#cx { session = [] :: [] | binary(),
formatter = bert :: bert | json,
actions = [] :: list(tuple()),
state = [] :: [] | term(),
module = [] :: [] | atom(),
lang = [] :: [] | atom(),
path = [] :: [] | binary(),
node = [] :: [] | atom(),
pid = [] :: [] | pid(),
vsn = [] :: [] | integer() }).
.fi
.SH PROTOCOL
.LP
While all application protocols in the system are desired
to have single effect environment or same error handling path,
n2o
defines a single protocol loop for all applications
as stack of protocols.
.LP
In core bundle
n2o
is shipped with NITRO and FTP protocols
which allows you to create real-time web applications with
binary-based protocols, also providing robust and performant
upload client and file transfer protocol. For building
web-based NITRO applications, you need to include
nitro
dependency.
info(term(),term(),#cx{}) -> #reply{} | #unknown{}.
.LP
The
info/3
is an N2O protocol callback that to be called
on each incoming request.
.SH RPC MQTT
.LP
N2O provides RPC over MQ mechanism for MQTT devices.
N2O spawns a set of
\fIn2o_mqtt\fR\&workers
as
\fIn2o_pi\fR\&processes that listen to
events topic. The responses send to actions topic, which is
subscribed automatically on MQTT session init.
.nf
actions/:vsn/:module/:client
events/:vsn/:node/:module/:client
.fi
.SH RPC WebSocket
.LP
In pure WebSocket case, N2O implements
\fIn2o_stream\fR\&as cowboy module supporting binary and text messages.
.nf
#binary { data :: binary() }.
#text { data :: binary() }.
.fi
.SH EXAMPLE
.LP
Here is an example of overriding INIT protocol.
.nf
-module(custom_init).
-include("n2o.hrl").
-export([info/3]).
info({text,<<"N2O,",Pickle/binary>>}, Req, State) ->
{'Token',Token} = n2o_session:authenticate([],Pickle),
Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
New = State#cx{session = Sid},
{reply,{bert,{io,<<"console.log('connected')">>,
{'Token',Token}}}, Req, New};
info(Message,Req,State) -> {unknown,Message,Req,State}.
.fi
.SH CONFIG
.LP
Just put protocol implementation module name to
protocol
option in sys.config.
.nf
[{n2o,[{cache,n2o},
{upload,"priv/static"},
{mq,n2o_syn},
{ttl,900},
{timer,{0,1,0}}
{tables,[cookies,file,caching,ring,async]},
{hmac,sha256},
{filename,n2o_ftp},
{formatter,n2o_bert},
{session,n2o_session},
{pickler,n2o_secret},
{protocols,[n2o_ftp,n2o_nitro]},
{nitro_prolongate,false},
{filter,{n2o_proto,push}},
{origin,<<"*">>},
{timer,{0,10,0}}]}].
.fi
.LP
N2O is the facade of the following services: cache, MQ, message formatting,
sessions, pickling and protocol loops. The other part of N2O is
\fIn2o_pi\fR\&module
for spawning supervised application processes to use N2O API. In this simple
configuration, you may set any implementation for any service.
.LP
The following configurable services are publically available in
n2o
module:
.SH CACHE
.LP
Cache is a fast expirable memory store. Just put values onto keys using
these functions and system timer will clear expired entries eventually.
You can select caching module implementation by setting cache n2o parameter
to the module name. Default n2o cache implementation turns each ets store
into expirable.
cache(Tab, Key, Value, Till) -> term().
.LP
Sets a Value with a given TTL.
cache(Tab, Key) -> term().
.LP
Gets a Value.
.SH MQ
.LP
The minimal requirement for any framework is the pub/sub API.
N2O provides selectable API through
mq
environment parameter.
reg(term()) -> term().
.LP
Subscribe a current client to a transient topic. In particular
implementation, the semantics could differ. In MQTT you can
subscribe offline/online clients to any persistent topic. Also in MQTT
this function subscribes MQTT client not an Erlang processe.
unreg(term()) -> term().
.LP
Unsubscribe a current client from a transient topic.
In MQTT we remove the subscription from the persistent database.
send(term(), term()) -> term().
.LP
Publish a message to a topic. In MQTT if clients are offline,
they will receive offline messages from the in-flight storage
once they become online.
.SH FORMAT
.LP
You specify the formatter in the protocol return message. E.g:
.nf
info({Code}, Req, State) ->
{reply,{bert,{io,nitro:jse(Code),<<>>}}, Req, State};
.fi
encode(record()) -> binary().
.LP
Serializes a record.
decode(binary()) -> record().
.LP
Deserializes a record.
.LP
Here is an example of
n2o_bert
formatter implementation.
.nf
encode(Erl) -> term_to_binary(Erl).
decode(Bin) -> binary_to_term(Bin,[safe]).
.fi
.SH SESSION
.LP
Sessions are stored in issued tokens encripted with AES/CBC-128.
All session variables are cached in ETS table in the default
implementation
\fIn2o_session\fR\&.
session(Key, Value) -> term().
.LP
Sets a value to session variable.
.nf
1> rr(n2o).
[bin,client,cx,direct,ev,flush,ftp,ftpack,handler,
mqtt_client,mqtt_message,pickle,server]
2> put(context,#cx{}).
undefined
3> n2o:session(user,maxim).
maxim
4> ets:tab2list(cookies).
[{{[],user},{63710014344,"maxim"}},
{{<<"5842b7e749a8cf44c920">>,auth},{63710014069,[]}]
.fi
session(Key) -> term().
.LP
Gets a value of session variable.
.SH PICKLE
pickle(term()) -> binary().
.LP
Custom Erlang term serialization.
depickle(binary()) -> term().
.LP
Custom Erlang term deserialization.
.SH ALSO
.LP
\fB\fIn2o_pi(1)\fR\&\fR\&, \fB\fIn2o_auth(1)\fR\&\fR\&, \fB\fIn2o_stream(1)\fR\&\fR\&, \fB\fIn2o_mqtt(1)\fR\&\fR\&, \fB\fIn2o_proto(1)\fR\&\fR\&
<!DOCTYPE html><html><head><meta charset="utf-8" /><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="description" content="" /><meta name="author" content="Maxim Sokhatsky" /><title>N2O</title><link rel="stylesheet" href="https://synrc.space/synrc.css" /></head><body><nav>
<a href="https://n2o.dev">DEV</a>
<a href="https://ws.n2o.space">N2O</a>
<a href="#" style="background:#ededed;">N2O</a>
<a href="ua/n2o.htm">UA</a>
<a href="#" style="background:#ededed;">EN</a>
</nav><header>
<a href="../index.html"><img src="https://n2o.space/img/Synrc Neo.svg" /></a>
<h1>N2O</h1>
</header><main>
<section>
<h3>INTRO</h3>
<p>The <a href="https://github.com/synrc/n2o/blob/master/src/n2o.erl">n2o</a>
defines the way you create, configure and run
arbitrary applications and protocols inside some hosts, into
which N2O can be injected, such as
<a href="https://github.com/ninenines/cowboy">cowboy</a>
and <a href="http://github.com/synrc/emqttd">emqttd</a>.
Each application can spawn its instance in its way like
web pages spawn WebSocket connections, workflow engines
spawn business processes, and chat applications spawns roster
and chatroom processes. With N2O everything is managed by protocols.</p>
<center><img src="N2O.svg" width="60%" /></center>
<p>N2O shipped to work in two modes:
1) inside <a href="n2o_mqtt.htm">n2o_mqtt</a> workers;
2) inside cowboy processes, implemented in <a href="n2o_stream.htm">n2o_stream</a>.
In the first case, the MQTT server used between clients and server workers.
In the second case, no more Erlang processes introduced except clients.
You can create your configuration of N2O processing loop.</p>
<p><img src="WebSocket + MQTT.svg" width="100%" /></p>
<p>The N2O itself is an embeddable protocol loop in <a href="n2o_proto.htm">n2o_proto</a>.
However, besides that, it handles cache and sessions
along with flexible <a href="n2o_pi.htm">n2o_pi</a> processes with no ownership restriction.
It also introduces AES/CBC—128 pickling and BERT/JSON encoder.</p>
</section>
<section>
<h3>TYPES</h3>
<figure><code>
-type formatter() :: binary | json | bert | text | default | atom().
-type response() :: { formatter(), binary() }.
</code></figure>
<figure><figcaption>Listing 1. Erlang/OTP records</figcaption><code>
#ok { data = [] :: term() }.
#error { data = [] :: term() }.
</code></figure>
<figure><figcaption>Listing 2. N2O Protocol</figcaption><code>
#reply { resp = [] :: [] | response(),
req = [] :: [] | term(),
state = [] :: [] | term() }.
#unknown { data = [] :: [] | binary(),
req = [] :: [] | term(),
state = [] :: [] | term() }.
</code></figure>
<figure><figcaption>Listing 3. N2O State</figcaption><code>
#cx { session = [] :: [] | binary(),
formatter = bert :: bert | json,
actions = [] :: list(tuple()),
state = [] :: [] | term(),
module = [] :: [] | atom(),
lang = [] :: [] | atom(),
path = [] :: [] | binary(),
node = [] :: [] | atom(),
pid = [] :: [] | pid(),
vsn = [] :: [] | integer() }).
</code></figure>
</section>
<section>
<h3>PROTOCOL</h3>
<p>While all application protocols in the system are desired
to have single effect environment or same error handling path,
<b>n2o</b> defines a single protocol loop for all applications
as stack of protocols.</p>
<p>In core bundle <b>n2o</b> is shipped with NITRO and FTP protocols
which allows you to create real-time web applications with
binary-based protocols, also providing robust and performant
upload client and file transfer protocol. For building
web-based NITRO applications, you need to include <b>nitro</b> dependency.</p>
<h4>info(term(),term(),#cx{}) -> #reply{} | #unknown{}.</h4>
<p>The <b>info/3</b> is an N2O protocol callback that to be called
on each incoming request.</p>
</section>
<section>
<h3>RPC MQTT</h3>
<p>N2O provides RPC over MQ mechanism for MQTT devices.
N2O spawns a set of <a href="n2o_mqtt.htm">n2o_mqtt</a> workers
as <a href="n2o_pi.htm">n2o_pi</a> processes that listen to
events topic. The responses send to actions topic, which is
subscribed automatically on MQTT session init.</p>
<figure><figcaption>Listing 5. MQTT RPC Topics</figcaption><code>
actions/:vsn/:module/:client
events/:vsn/:node/:module/:client
</code></figure>
</section>
<section>
<h3>RPC WebSocket</h3>
<p>In pure WebSocket case, N2O implements <a href="n2o_stream.htm">n2o_stream</a>
as cowboy module supporting binary and text messages.</p>
<figure><figcaption>Listing 6. Cowboy stream protocol</figcaption><code>
#binary { data :: binary() }.
#text { data :: binary() }.
</code></figure>
</section>
<section>
<h3>EXAMPLE</h3>
<p>Here is an example of overriding INIT protocol.</p>
<figure><figcaption>Listing 7. Custom INIT Protocol</figcaption><code>
-module(custom_init).
-include("n2o.hrl").
-export([info/3]).
info({text,&lt;&lt;"N2O,",Pickle/binary>>}, Req, State) ->
{'Token',Token} = n2o_session:authenticate([],Pickle),
Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
New = State#cx{session = Sid},
{reply,{bert,{io,&lt;&lt;"console.log('connected')">>,
{'Token',Token}}}, Req, New};
info(Message,Req,State) -> {unknown,Message,Req,State}.
</code></figure>
</section>
<section>
<h3>CONFIG</h3>
<p>Just put protocol implementation module name to <b>protocol</b> option in sys.config.</p>
<figure><code>
[{n2o,[{cache,n2o},
{upload,"priv/static"},
{mq,n2o_syn},
{ttl,900},
{timer,{0,1,0}}
{tables,[cookies,file,caching,ring,async]},
{hmac,sha256},
{filename,n2o_ftp},
{formatter,n2o_bert},
{session,n2o_session},
{pickler,n2o_secret},
{protocols,[n2o_ftp,n2o_nitro]},
{nitro_prolongate,false},
{filter,{n2o_proto,push}},
{origin,&lt;&lt;"*">>},
{timer,{0,10,0}}]}].
</code></figure>
<p>N2O is the facade of the following services: cache, MQ, message formatting,
sessions, pickling and protocol loops. The other part of N2O is <a href="n2o_pi.htm">n2o_pi</a> module
for spawning supervised application processes to use N2O API. In this simple
configuration, you may set any implementation for any service.</p>
</section>
<section>
<p>The following configurable services are publically available in <b>n2o</b> module:</p>
</section>
<section>
<h3>CACHE</h3>
<p>Cache is a fast expirable memory store. Just put values onto keys using
these functions and system timer will clear expired entries eventually.
You can select caching module implementation by setting cache n2o parameter
to the module name. Default n2o cache implementation turns each ets store
into expirable.</p>
<h4>cache(Tab, Key, Value, Till) -> term().</h4>
<p>Sets a Value with a given TTL.</p>
<h4>cache(Tab, Key) -> term().</h4>
<p>Gets a Value.</p>
</section>
<section>
<h3>MQ</h3>
<p>The minimal requirement for any framework is the pub/sub API.
N2O provides selectable API through <b>mq</b> environment parameter.</p>
<h4>reg(term()) -> term().</h4>
<p>Subscribe a current client to a transient topic. In particular
implementation, the semantics could differ. In MQTT you can
subscribe offline/online clients to any persistent topic. Also in MQTT
this function subscribes MQTT client not an Erlang processe.</p>
<h4>unreg(term()) -> term().</h4>
<p>Unsubscribe a current client from a transient topic.
In MQTT we remove the subscription from the persistent database.</p>
<h4>send(term(), term()) -> term().</h4>
<p>Publish a message to a topic. In MQTT if clients are offline,
they will receive offline messages from the in-flight storage
once they become online.</p>
</section>
<section>
<h3>FORMAT</h3>
<p>You specify the formatter in the protocol return message. E.g:</p>
<figure><code>
info({Code}, Req, State) ->
{reply,{bert,{io,nitro:jse(Code),&lt;&lt;>>}}, Req, State};
</code></figure>
<h4>encode(record()) -> binary().</h4>
<p>Serializes a record.</p>
<h4>decode(binary()) -> record().</h4>
<p>Deserializes a record.</p>
<p>Here is an example of <b>n2o_bert</b> formatter implementation.</p>
<figure><code>
encode(Erl) -> term_to_binary(Erl).
decode(Bin) -> binary_to_term(Bin,[safe]).
</code></figure>
</section>
<section>
<h3>SESSION</h3>
<p>Sessions are stored in issued tokens encripted with AES/CBC-128.
All session variables are cached in ETS table in the default
implementation <a href="n2o_session.htm">n2o_session</a>.
</p>
<h4>session(Key, Value) -> term().</h4>
<p>Sets a value to session variable.</p>
<figure><figcaption>Listing 8. Sessions</figcaption><code>
1> rr(n2o).
[bin,client,cx,direct,ev,flush,ftp,ftpack,handler,
mqtt_client,mqtt_message,pickle,server]
2> put(context,#cx{}).
undefined
3> n2o:session(user,maxim).
maxim
4> ets:tab2list(cookies).
[{{[],user},{63710014344,"maxim"}},
{{&lt;&lt;"5842b7e749a8cf44c920">>,auth},{63710014069,[]}]
</code></figure>
<h4>session(Key) -> term().</h4>
<p>Gets a value of session variable.</p>
</section>
<section>
<h3>PICKLE</h3>
<h4>pickle(term()) -> binary().</h4>
<p>Custom Erlang term serialization.</p>
<h4>depickle(binary()) -> term().</h4>
<p>Custom Erlang term deserialization.</p>
</section>
<section>
<p>This module may refer to:
<a href="n2o_pi.htm">n2o_pi</a>,
<a href="n2o_auth.htm">n2o_auth</a>,
<a href="n2o_stream.htm">n2o_stream</a>,
<a href="n2o_mqtt.htm">n2o_mqtt</a>,
<a href="n2o_proto.htm">n2o_proto</a>.
</p>
</section>
</main><footer>
2005—2019 © Synrc Research Center
</footer></body></html>
-module(v0).
-include_lib("xmerl/include/xmerl.hrl").
% /usr/lib/erlang/lib/xmerl-1.3.21/include/xmerl.hrl
%-import(xmerl_xs, [ xslapply/2, value_of/1, select/2, built_in_rules/2 ]).
-compile([export_all, nowarn_export_all]).
do() ->
% pass head && take body
%{#xmlElement{content=[HeadTree, BodyTree | _]}, _Misc} = xmerl_scan:file("n2o.htm"),
FileName = "n2o.htm",
{#xmlElement{content=[#xmlElement{content=HeadContent}, BodyTree | _]}, _Misc} = xmerl_scan:file(FileName),
%.TH n2o 1 "n2o 4.5.0" "Synrc Research Center" "N2O"
%.SH NAME
%n2o \- Protocol and Application Server
FN = hd( string:split(FileName, ".", leading) ),
write2new(FN, lists:reverse( show_node(BodyTree, false,
[ [".TH ", FN, " 1 \"n2o 4.5.0\" \"Synrc Research Center\" \"", get_head_title(HeadContent, ""), "\"", "\n",
".SH NAME", "\n",
"n2o \\- Protocol and Application Server", "\n"] ]) ) ),
ok.
%show_node(Tree, Is_Inside_Section, ResultAcc) -> % Is_Inside_Section = false (not inside section) | {true, last} | {true, usual} | {true, other}
show_node(#xmlElement{name=section, attributes=_Attributes, content=Content}, false, ResultAcc) ->
% take section
Is_Inside_Section = check_section_type(Content, false),
show_children(Content, Is_Inside_Section, [title_if_last_section(Is_Inside_Section) | ResultAcc]);
show_node(#xmlElement{name=_, content=Content}, false, ResultAcc) ->
% pass -- not section elem, not inside section
show_children(Content, false, ResultAcc);
show_node(#xmlElement{name=h3, attributes=_Attributes, content=Content}, {true, Section}, ResultAcc) ->
% not section elem, inside section -- h3 = title, add groff section title tag
show_children(Content, {true, Section}, [".SH ", "\n" | ResultAcc]);
show_node(#xmlElement{name=a, attributes=_Attributes, content=[#xmlText{value=Value} | _ContentMore]}, {true, last}, ResultAcc) ->
% not section elem, inside last section -- take url text -- a > text, add groff tag for last section a text
% ("\fB\fIURL(1)\fR\&\fR\&," ->"\\fB\\fIURL(1)\\fR\\&\\fR\\&," -> )
[["\\fB\\fI", Value, "(1)", "\\fR\\&\\fR\\&", ", "] | ResultAcc];
show_node(#xmlElement{name=a, attributes=_Attributes, content=[#xmlText{value=Value} | ContentMore]}, {true, Section}, ResultAcc) ->
% not section elem, inside section -- a = url, take next node, add groff tag for url ( "\fI" -> "\\fI", "\fR\&" -> "\\fR\\&")
show_children(ContentMore, {true, Section}, [["\\fI", Value, "\\fR\\&"] | ResultAcc]);
show_node(#xmlElement{name=code, attributes=_Attributes, content=[#xmlText{value=Value} | ContentMore]}, {true, Section}, ResultAcc) ->
% not section elem, inside section -- code, take next node -- text, add groff tags for code
show_children(ContentMore, {true, Section}, [[".nf", "\n", text_value_lines_trim(Value), ".fi", "\n"] | ResultAcc]);
show_node(#xmlElement{name=p, attributes=_Attributes, content=Content}, {true, Section}, ResultAcc) ->
% not section elem, inside section -- p, add groff p tag
show_children(Content, {true, Section}, ["\n", ".LP" | ResultAcc]);
show_node(#xmlElement{name=figcaption, attributes=_Attributes, content=_Content}, _Is_Inside_Section, ResultAcc) ->
% not section elem, inside section -- figcaption, pass
ResultAcc;
%show_node(#xmlElement{name=Name, attributes=_Attributes, content=Content}, Is_Inside_Section, ResultAcc) ->
show_node(#xmlElement{name=_Name, content=Content}, Is_Inside_Section, ResultAcc) ->
% not section elem, inside section
%io:format("name 777: ~s~n", [Name]),
show_children(Content, Is_Inside_Section, ResultAcc);
show_node(_, false, ResultAcc) ->
% pass -- text not inside section
ResultAcc;
show_node(_, {true, last}, ResultAcc) ->
% pass useless text inside last section
ResultAcc;
show_node(#xmlText{value=Value}, _Is_Inside_Section, ResultAcc) ->
G = (hd(Value) == 10) andalso (string:trim(tl(Value)) == ""),
if G ->
% pass "\n " % hd("\n") == 10
ResultAcc;
true ->
% text
%io:format("Text: ~p~n", [Value]),
[text_value_lines_trim(Value) | ResultAcc]
end.
% is section "usual" -- usual --
% section > h3 > text
% is section last -- "this module may refer to" -- last --
% section > first child = p > first child = text, second child = a
% otherwise section is "unusual" -- other
%check_section_type(Tree, Is_Child) -> % [Node | MoreNodes] = Tree
check_section_type([#xmlElement{name=h3, content=Content} | _MoreNodes], false) ->
check_section_type(Content, {true, h3});
check_section_type([#xmlElement{name=p, content=Content} | _MoreNodes], false) ->
check_section_type(Content, {true, p});
check_section_type([#xmlText{value=Value} | MoreNodes], false) ->
G = (hd(Value) == 10) andalso (string:trim(tl(Value)) == ""),
if G ->
% pass "\n " % hd("\n") == 10
check_section_type(MoreNodes, false);
true ->
% text
{true, other}
end;
check_section_type([], {true, h3}) ->
{true, other};
check_section_type([#xmlText{value=_Value} | _MoreNodes], {true, h3}) ->
{true, usual};
check_section_type([_|_MoreNodes], {true, h3}) ->
{true, other};
check_section_type([#xmlText{value=_Value}, #xmlElement{name=a} | _MoreNodes], {true, p}) ->
{true, last};
check_section_type(_, _) ->
{true, other}.
show_children([], _, ResultAcc) -> ResultAcc;
show_children([Node | MoreNodes], Is_Inside_Section, ResultAcc) ->
%ResultAcc2 = show_node(Node, Is_Inside_Section, ResultAcc),
%show_children(MoreNodes, Is_Inside_Section, ResultAcc2).
show_children(MoreNodes, Is_Inside_Section, show_node(Node, Is_Inside_Section, ResultAcc)).
title_if_last_section({true, last}) -> ["\n", ".SH ALSO", "\n"];
title_if_last_section(_) -> "".
text_value_lines_trim(V) ->
[[string:trim(V2, both), "\n"] || V2 <- string:split(V, "\n", all), string:trim(V2, both) =/= ""].
get_head_title([], A) -> A;
get_head_title([#xmlElement{name=title, content=[#xmlText{value=Value} | _]} | _], _) -> Value;
get_head_title([_|T], A) -> get_head_title(T, A).
write2new(F, S) ->
file:write_file(F ++ ".1", io_lib:fwrite("~s", [string:trim( unicode:characters_to_binary(S,utf8), trailing, ", ")]), [append]).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment