25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-20 16:15:59 +01:00

* src/web/ejabberd_web_admin.erl: Use textareas for large input

like ejabberd module options and listening port options. Show
result of POST more clearly. Ensure access rules are shown with
some minimum separation. Improve menu headers. (EJAB-562)
* src/web/ejabberd_web_admin.hrl: Likewise
* src/mod_offline.erl: Likewise
* src/mod_offline_odbc.erl: Likewise
* src/mod_roster.erl: Likewise
* src/mod_roster_odbc.erl: Likewise
* src/mod_shared_roster.erl: Likewise

SVN Revision: 1813
This commit is contained in:
Badlop 2009-01-12 21:48:34 +00:00
parent ba2eb35590
commit 43a98ab69b
8 changed files with 142 additions and 95 deletions

View File

@ -1,5 +1,16 @@
2009-01-12 Badlop <badlop@process-one.net>
* src/web/ejabberd_web_admin.erl: Use textareas for large input
like ejabberd module options and listening port options. Show
result of POST more clearly. Ensure access rules are shown with
some minimum separation. Improve menu headers. (EJAB-562)
* src/web/ejabberd_web_admin.hrl: Likewise
* src/mod_offline.erl: Likewise
* src/mod_offline_odbc.erl: Likewise
* src/mod_roster.erl: Likewise
* src/mod_roster_odbc.erl: Likewise
* src/mod_shared_roster.erl: Likewise
* src/ejabberd_listener.erl: New way to configure IP address and
IP version of listener. Support for definition of IP address in
string format, and implicit definition of IP
@ -13,6 +24,7 @@
* src/mod_proxy65/mod_proxy65_stream.erl: Likewise
* src/mod_proxy65/mod_proxy65_service.erl: Likewise
* src/web/ejabberd_web_admin.erl: Likewise
* doc/guide.tex: Document the new way to configure IP address and
IP version of listener, undocument options ip and inet6
* doc/guide.html: Likewise

View File

@ -500,7 +500,7 @@ user_queue(User, Server, Query, Lang) ->
[?XC("h1", io_lib:format(?T("~s's Offline Messages Queue"),
[us_to_list(US)]))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
ok -> [?XREST("Submitted")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],

View File

@ -358,7 +358,7 @@ user_queue(User, Server, Query, Lang) ->
[?XC("h1", io_lib:format(?T("~s's Offline Messages Queue"),
[us_to_list(US)]))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
ok -> [?XREST("Submitted")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],

View File

@ -844,8 +844,8 @@ user_roster(User, Server, Query, Lang) ->
end,
[?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],

View File

@ -944,8 +944,8 @@ user_roster(User, Server, Query, Lang) ->
end,
[?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],

View File

@ -725,8 +725,8 @@ list_shared_roster_groups(Host, Query, Lang) ->
)]),
?H1GL(?T("Shared Roster Groups"), "modsharedroster", "mod_shared_roster") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -789,8 +789,9 @@ shared_roster_group(Host, Group, Query, Lang) ->
[]
end ++ [[us_to_list(Member), $\n] || Member <- Members],
FDisplayedGroups = [[DG, $\n] || DG <- DisplayedGroups],
DescNL = length(element(2, regexp:split(Description, "\n"))),
FGroup =
?XAE("table", [],
?XAE("table", [{"class", "withtextareas"}],
[?XE("tbody",
[?XE("tr",
[?XCT("td", "Name:"),
@ -799,34 +800,34 @@ shared_roster_group(Host, Group, Query, Lang) ->
),
?XE("tr",
[?XCT("td", "Description:"),
?XE("td", [?XAC("textarea", [{"name", "description"},
{"rows", "3"},
{"cols", "20"}],
Description)])
?XE("td", [
?TEXTAREA("description", integer_to_list(lists:max([3, DescNL])), "20", Description)
]
)
]
),
?XE("tr",
[?XCT("td", "Members:"),
?XE("td", [?XAC("textarea", [{"name", "members"},
{"rows", "3"},
{"cols", "20"}],
FMembers)])
?XE("td", [
?TEXTAREA("members", integer_to_list(lists:max([3, length(FMembers)])), "20", FMembers)
]
)
]
),
?XE("tr",
[?XCT("td", "Displayed Groups:"),
?XE("td", [?XAC("textarea", [{"name", "dispgroups"},
{"rows", "3"},
{"cols", "20"}],
FDisplayedGroups)])
?XE("td", [
?TEXTAREA("dispgroups", integer_to_list(lists:max([3, length(FDisplayedGroups)])), "20", FDisplayedGroups)
]
)
]
)]
)]),
?H1GL(?T("Shared Roster Groups"), "modsharedroster", "mod_shared_roster") ++
[?XC("h2", ?T("Group ") ++ Group)] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],

View File

@ -157,7 +157,7 @@ make_xhtml(Els, Host, Node, Lang) ->
[?XAE("div",
[{"id", "header"}],
[?XE("h1",
[?ACT("/admin/", "Administration")]
[?ACT("/admin/", "ejabberd Web Admin")]
)]),
?XAE("div",
[{"id", "navigation"}],
@ -312,6 +312,7 @@ ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {
text-align: center;
border-top: 2px solid #d47911;
border-bottom: 1px solid #d47911;
background: #FED6A6;
}
#navheadsub, #navitemsub {
@ -367,10 +368,6 @@ textarea {
border: 1px solid #d6760e;
color: #723202;
background-color: #fff2e8;
vertical-align: middle;
margin-top: 7px;
margin-bottom: 5px;
padding: 0.1em;
}
select {
@ -538,6 +535,19 @@ div.guidelink {
padding-right: 1em;
}
table.withtextareas>tbody>tr>td {
vertical-align: top;
}
p.result {
border: 1px;
border-style: dashed;
border-color: #FE8A02;
padding: 1em;
margin-right: 1em;
background: #FFE3C9;
}
*.alignright {
font-size: 10pt;
}
@ -633,6 +643,7 @@ process_admin(Host,
#request{path = ["acls-raw"],
q = Query,
lang = Lang}) ->
Res = case lists:keysearch("acls", 1, Query) of
{value, {_, String}} ->
case erl_scan:string(String) of
@ -654,22 +665,17 @@ process_admin(Host,
_ ->
nothing
end,
ACLs = lists:flatten(
io_lib:format(
"~p.", [lists:keysort(
2, ets:select(acl, [{{acl, {'$1', Host}, '$2'},
[], [{{acl, '$1', '$2'}}]}]))])),
ACLs = lists:keysort(2, ets:select(acl, [{{acl, {'$1', Host}, '$2'},
[], [{{acl, '$1', '$2'}}]}])),
{NumLines, ACLsP} = term_to_paragraph(ACLs, 80),
make_xhtml(?H1GL(?T("Access Control Lists"), "ACLDefinition", "ACL Definition") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
[?XAC("textarea", [{"name", "acls"},
{"rows", "16"},
{"cols", "80"}],
ACLs),
[?TEXTAREA("acls", integer_to_list(lists:max([16, NumLines])), "80", ACLsP++"."),
?BR,
?INPUTT("submit", "submit", "Submit")
])
@ -704,8 +710,8 @@ process_admin(Host,
[], [{{acl, '$1', '$2'}}]}])),
make_xhtml(?H1GL(?T("Access Control Lists"), "ACLDefinition", "ACL Definition") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XE("p", [?ACT("../acls-raw/", "Raw")])] ++
@ -764,23 +770,19 @@ process_admin(Host,
nothing
end,
Access =
lists:flatten(
io_lib:format(
"~p.", [ets:select(config,
ets:select(config,
[{{config, {access, '$1', Host}, '$2'},
[],
[{{access, '$1', '$2'}}]}])])),
[{{access, '$1', '$2'}}]}]),
{NumLines, AccessP} = term_to_paragraph(Access, 80),
make_xhtml(?H1GL(?T("Access Rules"), "AccessRights", "Access Rights") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
[?XAC("textarea", [{"name", "access"},
{"rows", "16"},
{"cols", "80"}],
Access),
[?TEXTAREA("access", integer_to_list(lists:max([16, NumLines])), "80", AccessP++"."),
?BR,
?INPUTT("submit", "submit", "Submit")
])
@ -810,8 +812,8 @@ process_admin(Host,
[{{access, '$1', '$2'}}]}]),
make_xhtml(?H1GL(?T("Access Rules"), "AccessRights", "Access Rights") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XE("p", [?ACT("../access-raw/", "Raw")])] ++
@ -850,8 +852,8 @@ process_admin(Host,
make_xhtml([?XC("h1",
io_lib:format(?T("~s access rule configuration"), [SName]))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -1054,12 +1056,20 @@ acl_spec_select(ID, Opt) ->
node_regexp, user_glob, server_glob, node_glob, all, raw]))]).
%% @spec (T::any()) -> StringLine::string()
term_to_string(T) ->
StringParagraph = lists:flatten(io_lib:format("~1000000p", [T])),
%% Remove from the string all the carriage returns characters
{ok, StringLine, _} = regexp:gsub(StringParagraph, "\\n ", ""),
StringLine.
%% @spec (T::any()) -> {NumLines::integer(), Paragraph::string()}
term_to_paragraph(T, Cols) ->
Paragraph = erl_prettypr:format(erl_syntax:abstract(T), [{paper, Cols}]),
{ok, FieldList} = regexp:split(Paragraph, "\n"),
NumLines = length(FieldList),
{NumLines, Paragraph}.
term_to_id(T) ->
jlib:encode_base64(binary_to_list(term_to_binary(T))).
@ -1227,7 +1237,7 @@ access_rule_to_xhtml(Rules) ->
fun({Access, ACL} = _Rule) ->
SAccess = element_to_list(Access),
SACL = atom_to_list(ACL),
SAccess ++ "\t" ++ SACL ++ "\n"
SAccess ++ "\s\t" ++ SACL ++ "\n"
end, Rules),
?XAC("textarea", [{"name", "rules"},
{"rows", "16"},
@ -1304,8 +1314,8 @@ list_users(Host, Query, Lang, URLFunc) ->
end, lists:seq(1, N, M))
end,
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -1500,8 +1510,8 @@ user_info(User, Server, Query, Lang) ->
[User, Server, Lang]),
[?XC("h1", ?T("User ") ++ us_to_list(US))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -1668,8 +1678,8 @@ get_node(global, Node, [], Query, Lang) ->
MenuItems2 = make_menu_items(global, Node, Base, Lang),
[?XC("h1", ?T("Node ") ++ atom_to_list(Node))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XE("ul",
@ -1698,7 +1708,10 @@ get_node(global, Node, ["db"], Query, Lang) ->
{badrpc, _Reason} ->
[?XCT("h1", "RPC Call Error")];
Tables ->
node_db_parse_query(Node, Tables, Query),
ResS = case node_db_parse_query(Node, Tables, Query) of
nothing -> [];
ok -> [?XREST("Submitted")]
end,
STables = lists:sort(Tables),
Rows = lists:map(
fun(Table) ->
@ -1735,7 +1748,7 @@ get_node(global, Node, ["db"], Query, Lang) ->
])
end, STables),
[?XC("h1", ?T("Database Tables at ") ++ atom_to_list(Node))] ++
[?CT("Submitted"), ?P] ++
ResS ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
[?XAE("table", [],
[?XE("thead",
@ -1757,9 +1770,14 @@ get_node(global, Node, ["db"], Query, Lang) ->
end;
get_node(global, Node, ["backup"], Query, Lang) ->
_Res = node_backup_parse_query(Node, Query),
[?XC("h1", ?T("Backup of ") ++ atom_to_list(Node)),
?XCT("p", "Remark that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately."),
ResS = case node_backup_parse_query(Node, Query) of
nothing -> [];
ok -> [?XREST("Submitted")];
{error, Error} -> [?XRES(?T("Error") ++": " ++ io_lib:format("~p", [Error]))]
end,
[?XC("h1", ?T("Backup of ") ++ atom_to_list(Node))] ++
ResS ++
[?XCT("p", "Remark that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately."),
?XAE("form", [{"action", ""}, {"method", "post"}],
[?XAE("table", [],
[?XE("tbody",
@ -1822,9 +1840,9 @@ get_node(global, Node, ["ports"], Query, Lang) ->
H1String = ?T("Listened Ports at ") ++ atom_to_list(Node),
?H1GL(H1String, "listened", "Listening Ports") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
{error, ReasonT} -> [?CT("Problem: "), ?C(ReasonT), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
{error, ReasonT} -> [?XRES(?T("Error") ++ ": " ++ ReasonT)];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -1847,8 +1865,8 @@ get_node(Host, Node, ["modules"], Query, Lang) when is_list(Host) ->
H1String = ?T("Modules at ") ++ atom_to_list(Node),
?H1GL(H1String, "modoverview", "Modules Overview") ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
@ -1916,17 +1934,19 @@ get_node(global, Node, ["update"], Query, Lang) ->
FmtLowLevelScript = ?XC("pre", io_lib:format("~p", [LowLevelScript])),
[?XC("h1", ?T("Update ") ++ atom_to_list(Node))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
error -> [?CT("Bad format"), ?P];
ok -> [?XREST("Submitted")];
error -> [?XREST("Bad format")];
nothing -> []
end ++
[?XAE("form", [{"action", ""}, {"method", "post"}],
[?INPUTT("submit", "update", "Update"),
[
?XCT("h2", "Update plan"),
?XCT("h3", "Updated modules"), Mods,
?XCT("h3", "Modified modules"), Mods,
?XCT("h3", "Update script"), FmtScript,
?XCT("h3", "Low level update script"), FmtLowLevelScript,
?XCT("h3", "Script check"), ?C(atom_to_list(Check))])
?XCT("h3", "Script check"), ?XC("pre", atom_to_list(Check)),
?INPUTT("submit", "update", "Update")
])
];
get_node(Host, Node, NPath, Query, Lang) ->
@ -1981,6 +2001,8 @@ db_storage_select(ID, Opt, Lang) ->
{disc_only_copies, "Disc only copy"},
{unknown, "Remote copy"}])).
node_db_parse_query(_Node, _Tables, [{nokey,[]}]) ->
nothing;
node_db_parse_query(Node, Tables, Query) ->
lists:foreach(
fun(Table) ->
@ -2014,6 +2036,8 @@ node_db_parse_query(Node, Tables, Query) ->
end, Tables),
ok.
node_backup_parse_query(_Node, [{nokey,[]}]) ->
nothing;
node_backup_parse_query(Node, Query) ->
lists:foldl(
fun(Action, nothing) ->
@ -2040,15 +2064,15 @@ node_backup_parse_query(Node, Query) ->
load_textfile, [Path])
end,
case Res of
{error, _Reason} ->
error;
{badrpc, _Reason} ->
error;
{error, Reason} ->
{error, Reason};
{badrpc, Reason} ->
{badrpc, Reason};
_ ->
ok
end;
_ ->
error
OtherError ->
{error, OtherError}
end;
_ ->
nothing
@ -2059,7 +2083,7 @@ node_backup_parse_query(Node, Query) ->
node_ports_to_xhtml(Ports, Lang) ->
?XAE("table", [],
?XAE("table", [{"class", "withtextareas"}],
[?XE("thead",
[?XE("tr",
[?XCT("td", "Port"),
@ -2073,14 +2097,14 @@ node_ports_to_xhtml(Ports, Lang) ->
{_Port, SPort, _TIP, SIP, SSPort, OptsClean} =
get_port_data(PortIP, Opts),
SModule = atom_to_list(Module),
{NumLines, SOptsClean} = term_to_paragraph(OptsClean, 40),
%%ID = term_to_id(E),
?XE("tr",
[?XAE("td", [{"size", "6"}], [?C(SPort)]),
?XAE("td", [{"size", "15"}], [?C(SIP)]),
?XE("td", [?INPUTS("text", "module" ++ SSPort,
SModule, "15")]),
?XE("td", [?INPUTS("text", "opts" ++ SSPort,
term_to_string(OptsClean), "40")]),
?XE("td", [?TEXTAREA("opts" ++ SSPort, integer_to_list(NumLines), "35", SOptsClean)]),
?XE("td", [?INPUTT("submit", "add" ++ SSPort,
"Update")]),
?XE("td", [?INPUTT("submit", "delete" ++ SSPort,
@ -2091,8 +2115,8 @@ node_ports_to_xhtml(Ports, Lang) ->
[?XE("tr",
[?XE("td", [?INPUTS("text", "portnew", "", "6")]),
?XE("td", [?INPUTS("text", "ipnew", "0.0.0.0", "15")]),
?XE("td", [?INPUTS("text", "modulenew", "", "17")]),
?XE("td", [?INPUTS("text", "optsnew", "[]", "40")]),
?XE("td", [?INPUTS("text", "modulenew", "", "15")]),
?XE("td", [?TEXTAREA("optsnew", "2", "35", "[]")]),
?XAE("td", [{"colspan", "2"}],
[?INPUTT("submit", "addnew", "Add New")])
]
@ -2170,7 +2194,7 @@ node_ports_parse_query(Node, Ports, Query) ->
end.
node_modules_to_xhtml(Modules, Lang) ->
?XAE("table", [],
?XAE("table", [{"class", "withtextareas"}],
[?XE("thead",
[?XE("tr",
[?XCT("td", "Module"),
@ -2180,11 +2204,11 @@ node_modules_to_xhtml(Modules, Lang) ->
lists:map(
fun({Module, Opts} = _E) ->
SModule = atom_to_list(Module),
{NumLines, SOpts} = term_to_paragraph(Opts, 40),
%%ID = term_to_id(E),
?XE("tr",
[?XC("td", SModule),
?XE("td", [?INPUTS("text", "opts" ++ SModule,
term_to_string(Opts), "40")]),
?XE("td", [?TEXTAREA("opts" ++ SModule, integer_to_list(NumLines), "40", SOpts)]),
?XE("td", [?INPUTT("submit", "restart" ++ SModule,
"Restart")]),
?XE("td", [?INPUTT("submit", "stop" ++ SModule,
@ -2194,7 +2218,7 @@ node_modules_to_xhtml(Modules, Lang) ->
end, Modules) ++
[?XE("tr",
[?XE("td", [?INPUT("text", "modulenew", "")]),
?XE("td", [?INPUTS("text", "optsnew", "", "40")]),
?XE("td", [?TEXTAREA("optsnew", "2", "40", "[]")]),
?XAE("td", [{"colspan", "2"}],
[?INPUTT("submit", "start", "Start")])
]
@ -2437,11 +2461,11 @@ make_menu_items2(Lang, Deep, {MURI, MName, [Item | Items]}, Res) ->
make_menu_items2(Lang, Deep, {MURI, MName, Items}, Res2).
make_menu_item(header, 1, URI, Name, _Lang) ->
?LI([?XAE("div", [{"id", "navhead"}], [?AC(URI, "~ "++Name++" ~")] )]);
?LI([?XAE("div", [{"id", "navhead"}], [?AC(URI, Name)] )]);
make_menu_item(header, 2, URI, Name, _Lang) ->
?LI([?XAE("div", [{"id", "navheadsub"}], [?AC(URI, "~ "++Name++" ~")] )]);
?LI([?XAE("div", [{"id", "navheadsub"}], [?AC(URI, Name)] )]);
make_menu_item(header, 3, URI, Name, _Lang) ->
?LI([?XAE("div", [{"id", "navheadsubsub"}], [?AC(URI, "~ "++Name++" ~")] )]);
?LI([?XAE("div", [{"id", "navheadsubsub"}], [?AC(URI, Name)] )]);
make_menu_item(item, 1, URI, Name, Lang) ->
?LI([?XAE("div", [{"id", "navitem"}], [?ACT(URI, Name)] )]);
make_menu_item(item, 2, URI, Name, Lang) ->

View File

@ -51,6 +51,16 @@
-define(INPUTST(Type, Name, Value, Size), ?INPUT(Type, Name, ?T(Value), Size)).
-define(ACLINPUT(Text), ?XE("td", [?INPUT("text", "value" ++ ID, Text)])).
-define(TEXTAREA(Name, Rows, Cols, Value),
?XAC("textarea", [{"name", Name},
{"rows", Rows},
{"cols", Cols}],
Value)).
%% Build an xmlelement for result
-define(XRES(Text), ?XAC("p", [{"class", "result"}], Text)).
-define(XREST(Text), ?XRES(?T(Text))).
%% Guide Link
-define(GL(Ref, Title),
?XAE("div",