dot: add x option for dot2tex
* spot/twa/acc.cc, spot/twa/acc.hh: Add a LaTeX output for acceptance conditions. * spot/twaalgos/dot.cc: Implement the 'x' option and refactor the code a bit to limit duplication. * tests/core/dot2tex.test: New test case (requires dot2tex). * tests/Makefile.am: Add dot2tex.test. * tests/core/alternating.test, tests/core/readsave.test, tests/python/automata-io.ipynb: Adjust expected output. * NEWS, doc/org/oaut.org: Mention the new option.
This commit is contained in:
parent
b242122ce8
commit
fbb9e4374e
10 changed files with 365 additions and 182 deletions
5
NEWS
5
NEWS
|
|
@ -75,6 +75,11 @@ New in spot 2.3.5.dev (not yet released)
|
||||||
We plan to enable 'a' by default in a future release, so a new
|
We plan to enable 'a' by default in a future release, so a new
|
||||||
option 'A' has been added to hide the acceptance condition.
|
option 'A' has been added to hide the acceptance condition.
|
||||||
|
|
||||||
|
- The print_dot() function has a new experimental option 'x' to
|
||||||
|
output labels are LaTeX formulas. This is meant to be used in
|
||||||
|
conjunction with the dot2tex tool. See
|
||||||
|
https://spot.lrde.epita.fr/oaut.html#dot2tex
|
||||||
|
|
||||||
- A new named property for automata called "original-states" can be
|
- A new named property for automata called "original-states" can be
|
||||||
used to record the origin of a state before transformation. It is
|
used to record the origin of a state before transformation. It is
|
||||||
currently defined by the degeneralization algorithms, and by
|
currently defined by the degeneralization algorithms, and by
|
||||||
|
|
|
||||||
|
|
@ -579,6 +579,8 @@ digraph G {
|
||||||
}
|
}
|
||||||
#+end_example
|
#+end_example
|
||||||
|
|
||||||
|
** Converting dot output to images or pdf
|
||||||
|
|
||||||
This output should be processed with =dot= to be converted into a
|
This output should be processed with =dot= to be converted into a
|
||||||
picture. For instance use =dot -Tpng= or =dot -Tpdf=.
|
picture. For instance use =dot -Tpng= or =dot -Tpdf=.
|
||||||
|
|
||||||
|
|
@ -589,6 +591,8 @@ $txt
|
||||||
#+RESULTS:
|
#+RESULTS:
|
||||||
[[file:oaut-dot1.png]]
|
[[file:oaut-dot1.png]]
|
||||||
|
|
||||||
|
** Customizing the dot output
|
||||||
|
|
||||||
This output can be customized by passing optional characters to the
|
This output can be customized by passing optional characters to the
|
||||||
=--dot= option. For instance =v= requests a vertical layout (instead
|
=--dot= option. For instance =v= requests a vertical layout (instead
|
||||||
of the default horizontal layout), =c= requests circle states, =s=
|
of the default horizontal layout), =c= requests circle states, =s=
|
||||||
|
|
@ -866,6 +870,63 @@ export SPOT_DOTDEFAULT='Brf(Lato)C(#ffffa0)'
|
||||||
export SPOT_DOTEXTRA='edge[arrowhead=vee, arrowsize=.7]'
|
export SPOT_DOTEXTRA='edge[arrowhead=vee, arrowsize=.7]'
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
** Working with =dot2tex=
|
||||||
|
:PROPERTIES:
|
||||||
|
:CUSTOM_ID: dot2tex
|
||||||
|
:END:
|
||||||
|
|
||||||
|
|
||||||
|
The [[https://github.com/kjellmf/dot2tex][=dot2tex= program]] interacts with GraphViz to converts dot files
|
||||||
|
into TeX figures. The layout is still done by tools provided by
|
||||||
|
GraphViz (i.e. =dot=, =neato=, =circo=, ...) but the actual rendering
|
||||||
|
is done using LaTeX with the TikZ or PSTricks packages. One advantage
|
||||||
|
is that this allows embedding math formulas into the graph, something
|
||||||
|
GraphViz alone cannot do. Another advantage is that you can then
|
||||||
|
easily edit the LaTeX figure, for instance to add additional graphical
|
||||||
|
elements.
|
||||||
|
|
||||||
|
The =dot= formater of Spot has an option =x=, that is convenient to
|
||||||
|
use with =dot2tex=. This option causes labels to be rendered as LaTeX
|
||||||
|
mathematical formulas instead of ASCII text.
|
||||||
|
|
||||||
|
#+BEGIN_SRC sh :exports code
|
||||||
|
ltl2tgba 'p0 U p1' --dot=x | dot2tex --autosize --nominsize > out.tex
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
The above command should give you a LaTeX file that compiles to the
|
||||||
|
following figure:
|
||||||
|
|
||||||
|
#+BEGIN_SRC sh :results silent :exports results
|
||||||
|
ltl2tgba 'p0 U p1' --dot=x | dot2tex --autosize --nominsize > dot2tex.tex
|
||||||
|
latexmk --pdf dot2tex.tex
|
||||||
|
convert -density 150 -trim dot2tex.pdf dot2tex.png
|
||||||
|
latexmk -C dot2tex.tex
|
||||||
|
rm -f dot2tex.tex
|
||||||
|
#+END_SRC
|
||||||
|
[[file:dot2tex.png]]
|
||||||
|
|
||||||
|
Caveats:
|
||||||
|
- =dot2tex= should be called with option =--autosize= in order to
|
||||||
|
compute the size of each label before calling GraphViz to layout the
|
||||||
|
graph. This is because GraphViz cannot compute the correct size of
|
||||||
|
mathematical formulas. Unfortunately, the release of =dot2tex
|
||||||
|
2.9.0= contains a bug where sizes are intepreted as integers instead
|
||||||
|
of floats. This can cause labels or shapes to disappear. This bug
|
||||||
|
of =dot2tex= was fixed in 2014, but at the time of writing
|
||||||
|
(summer 2017) no new release of =dot2tex= has been made. To work around this,
|
||||||
|
make sure you install =dot2tex= from its git repository:
|
||||||
|
#+BEGIN_SRC sh
|
||||||
|
git clone https://github.com/kjellmf/dot2tex.git
|
||||||
|
cd dot2tex
|
||||||
|
sudo python setup.py install
|
||||||
|
#+END_SRC
|
||||||
|
- The default size of nodes seems slightly too big for our usage.
|
||||||
|
Using =--nominsize= is just one way around it. Refer to the
|
||||||
|
[[https://dot2tex.readthedocs.io/en/latest/][=dot2tex= manual]] for finer ways to set the node size.
|
||||||
|
- Currently the =x= option of Spot's =--dot= output cannot yet be
|
||||||
|
combined with the =r=, =R=, an =b= options used to display colored
|
||||||
|
bullets. (Patches are welcome.)
|
||||||
|
|
||||||
* Statistics
|
* Statistics
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:CUSTOM_ID: stats
|
:CUSTOM_ID: stats
|
||||||
|
|
|
||||||
101
spot/twa/acc.cc
101
spot/twa/acc.cc
|
|
@ -63,20 +63,45 @@ namespace spot
|
||||||
os << v;
|
os << v;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<bool html>
|
enum code_output {HTML, TEXT, LATEX};
|
||||||
|
|
||||||
|
template<enum code_output style>
|
||||||
static void
|
static void
|
||||||
print_code(std::ostream& os,
|
print_code(std::ostream& os,
|
||||||
const acc_cond::acc_code& code, unsigned pos,
|
const acc_cond::acc_code& code, unsigned pos,
|
||||||
std::function<void(std::ostream&, int)> set_printer)
|
std::function<void(std::ostream&, int)> set_printer)
|
||||||
{
|
{
|
||||||
const char* op = " | ";
|
const char* op_ = style == LATEX ? " \\lor " : " | ";
|
||||||
auto& w = code[pos];
|
auto& w = code[pos];
|
||||||
const char* negated = "";
|
const char* negated_pre = "";
|
||||||
|
const char* negated_post = "";
|
||||||
|
auto set_neg = [&]() {
|
||||||
|
if (style == LATEX)
|
||||||
|
{
|
||||||
|
negated_pre = "\\overline{";
|
||||||
|
negated_post = "}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
negated_pre = "!";
|
||||||
|
}
|
||||||
|
};
|
||||||
bool top = pos == code.size() - 1;
|
bool top = pos == code.size() - 1;
|
||||||
switch (w.sub.op)
|
switch (w.sub.op)
|
||||||
{
|
{
|
||||||
case acc_cond::acc_op::And:
|
case acc_cond::acc_op::And:
|
||||||
op = html ? " & " : " & ";
|
switch (style)
|
||||||
|
{
|
||||||
|
case HTML:
|
||||||
|
op_ = " & ";
|
||||||
|
break;
|
||||||
|
case TEXT:
|
||||||
|
op_ = " & ";
|
||||||
|
break;
|
||||||
|
case LATEX:
|
||||||
|
op_ = " \\land ";
|
||||||
|
break;
|
||||||
|
}
|
||||||
SPOT_FALLTHROUGH;
|
SPOT_FALLTHROUGH;
|
||||||
case acc_cond::acc_op::Or:
|
case acc_cond::acc_op::Or:
|
||||||
{
|
{
|
||||||
|
|
@ -90,8 +115,8 @@ namespace spot
|
||||||
if (first)
|
if (first)
|
||||||
first = false;
|
first = false;
|
||||||
else
|
else
|
||||||
os << op;
|
os << op_;
|
||||||
print_code<html>(os, code, pos, set_printer);
|
print_code<style>(os, code, pos, set_printer);
|
||||||
pos -= code[pos].sub.size;
|
pos -= code[pos].sub.size;
|
||||||
}
|
}
|
||||||
if (!top)
|
if (!top)
|
||||||
|
|
@ -99,14 +124,17 @@ namespace spot
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case acc_cond::acc_op::InfNeg:
|
case acc_cond::acc_op::InfNeg:
|
||||||
negated = "!";
|
set_neg();
|
||||||
SPOT_FALLTHROUGH;
|
SPOT_FALLTHROUGH;
|
||||||
case acc_cond::acc_op::Inf:
|
case acc_cond::acc_op::Inf:
|
||||||
{
|
{
|
||||||
auto a = code[pos - 1].mark.id;
|
auto a = code[pos - 1].mark.id;
|
||||||
if (a == 0U)
|
if (a == 0U)
|
||||||
{
|
{
|
||||||
os << 't';
|
if (style == LATEX)
|
||||||
|
os << "\\mathsf{t}";
|
||||||
|
else
|
||||||
|
os << 't';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -115,16 +143,32 @@ namespace spot
|
||||||
top = code[pos - 1].mark.count() == 1;
|
top = code[pos - 1].mark.count() == 1;
|
||||||
unsigned level = 0;
|
unsigned level = 0;
|
||||||
const char* and_ = "";
|
const char* and_ = "";
|
||||||
|
const char* and_next_ = []() {
|
||||||
|
// The lack of surrounding space in HTML and
|
||||||
|
// TEXT is on purpose: we want to
|
||||||
|
// distinguish those grouped "Inf"s from
|
||||||
|
// other terms that are ANDed together.
|
||||||
|
switch (style)
|
||||||
|
{
|
||||||
|
case HTML:
|
||||||
|
return "&";
|
||||||
|
case TEXT:
|
||||||
|
return "&";
|
||||||
|
case LATEX:
|
||||||
|
return " \\land ";
|
||||||
|
}
|
||||||
|
}();
|
||||||
if (!top)
|
if (!top)
|
||||||
os << '(';
|
os << '(';
|
||||||
|
const char* inf_ = (style == LATEX) ? "\\mathsf{Inf}(" : "Inf(";
|
||||||
while (a)
|
while (a)
|
||||||
{
|
{
|
||||||
if (a & 1)
|
if (a & 1)
|
||||||
{
|
{
|
||||||
os << and_ << "Inf(" << negated;
|
os << and_ << inf_ << negated_pre;
|
||||||
set_printer(os, level);
|
set_printer(os, level);
|
||||||
os << ')';
|
os << negated_post << ')';
|
||||||
and_ = html ? "&" : "&";
|
and_ = and_next_;
|
||||||
}
|
}
|
||||||
a >>= 1;
|
a >>= 1;
|
||||||
++level;
|
++level;
|
||||||
|
|
@ -135,14 +179,17 @@ namespace spot
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case acc_cond::acc_op::FinNeg:
|
case acc_cond::acc_op::FinNeg:
|
||||||
negated = "!";
|
set_neg();
|
||||||
SPOT_FALLTHROUGH;
|
SPOT_FALLTHROUGH;
|
||||||
case acc_cond::acc_op::Fin:
|
case acc_cond::acc_op::Fin:
|
||||||
{
|
{
|
||||||
auto a = code[pos - 1].mark.id;
|
auto a = code[pos - 1].mark.id;
|
||||||
if (a == 0U)
|
if (a == 0U)
|
||||||
{
|
{
|
||||||
os << 'f';
|
if (style == LATEX)
|
||||||
|
os << "\\mathsf{f}";
|
||||||
|
else
|
||||||
|
os << 'f';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -153,14 +200,19 @@ namespace spot
|
||||||
const char* or_ = "";
|
const char* or_ = "";
|
||||||
if (!top)
|
if (!top)
|
||||||
os << '(';
|
os << '(';
|
||||||
|
const char* fin_ = (style == LATEX) ? "\\mathsf{Fin}(" : "Fin(";
|
||||||
while (a)
|
while (a)
|
||||||
{
|
{
|
||||||
if (a & 1)
|
if (a & 1)
|
||||||
{
|
{
|
||||||
os << or_ << "Fin(" << negated;
|
os << or_ << fin_ << negated_pre;
|
||||||
set_printer(os, level);
|
set_printer(os, level);
|
||||||
os << ')';
|
os << negated_post << ')';
|
||||||
or_ = "|";
|
// The lack of surrounding space in HTML and
|
||||||
|
// TEXT is on purpose: we want to distinguish
|
||||||
|
// those grouped "Fin"s from other terms that
|
||||||
|
// are ORed together.
|
||||||
|
or_ = style == LATEX ? " \\lor " : "|";
|
||||||
}
|
}
|
||||||
a >>= 1;
|
a >>= 1;
|
||||||
++level;
|
++level;
|
||||||
|
|
@ -1407,7 +1459,7 @@ namespace spot
|
||||||
if (empty())
|
if (empty())
|
||||||
os << 't';
|
os << 't';
|
||||||
else
|
else
|
||||||
print_code<true>(os, *this, size() - 1,
|
print_code<HTML>(os, *this, size() - 1,
|
||||||
set_printer ? set_printer : default_set_printer);
|
set_printer ? set_printer : default_set_printer);
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
@ -1420,7 +1472,20 @@ namespace spot
|
||||||
if (empty())
|
if (empty())
|
||||||
os << 't';
|
os << 't';
|
||||||
else
|
else
|
||||||
print_code<false>(os, *this, size() - 1,
|
print_code<TEXT>(os, *this, size() - 1,
|
||||||
|
set_printer ? set_printer : default_set_printer);
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream&
|
||||||
|
acc_cond::acc_code::to_latex(std::ostream& os,
|
||||||
|
std::function<void(std::ostream&, int)>
|
||||||
|
set_printer) const
|
||||||
|
{
|
||||||
|
if (empty())
|
||||||
|
os << "\\mathsf{t}";
|
||||||
|
else
|
||||||
|
print_code<LATEX>(os, *this, size() - 1,
|
||||||
set_printer ? set_printer : default_set_printer);
|
set_printer ? set_printer : default_set_printer);
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -892,6 +892,13 @@ namespace spot
|
||||||
std::function<void(std::ostream&, int)>
|
std::function<void(std::ostream&, int)>
|
||||||
set_printer = nullptr) const;
|
set_printer = nullptr) const;
|
||||||
|
|
||||||
|
// Print the acceptance as Latex. The set_printer function can
|
||||||
|
// be used to implement customized output for set numbers.
|
||||||
|
std::ostream&
|
||||||
|
to_latex(std::ostream& os,
|
||||||
|
std::function<void(std::ostream&, int)>
|
||||||
|
set_printer = nullptr) const;
|
||||||
|
|
||||||
|
|
||||||
/// \brief Construct an acc_code from a string.
|
/// \brief Construct an acc_code from a string.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <spot/tl/print.hh>
|
||||||
#include <spot/twa/twagraph.hh>
|
#include <spot/twa/twagraph.hh>
|
||||||
#include <spot/twaalgos/dot.hh>
|
#include <spot/twaalgos/dot.hh>
|
||||||
#include <spot/twa/bddprint.hh>
|
#include <spot/twa/bddprint.hh>
|
||||||
|
|
@ -100,6 +101,10 @@ namespace spot
|
||||||
bool opt_numbered_edges_ = false;
|
bool opt_numbered_edges_ = false;
|
||||||
bool opt_orig_show_ = false;
|
bool opt_orig_show_ = false;
|
||||||
bool max_states_given_ = false; // related to max_states_
|
bool max_states_given_ = false; // related to max_states_
|
||||||
|
bool opt_latex_ = false;
|
||||||
|
const char* nl_ = "\\n";
|
||||||
|
const char* label_pre_ = "label=\"";
|
||||||
|
char label_post_ = '"';
|
||||||
|
|
||||||
const_twa_graph_ptr aut_;
|
const_twa_graph_ptr aut_;
|
||||||
std::string opt_font_;
|
std::string opt_font_;
|
||||||
|
|
@ -265,6 +270,9 @@ namespace spot
|
||||||
case 't':
|
case 't':
|
||||||
opt_force_acc_trans_ = true;
|
opt_force_acc_trans_ = true;
|
||||||
break;
|
break;
|
||||||
|
case 'x':
|
||||||
|
opt_latex_ = true;
|
||||||
|
break;
|
||||||
case 'y':
|
case 'y':
|
||||||
opt_shared_univ_dest_ = false;
|
opt_shared_univ_dest_ = false;
|
||||||
break;
|
break;
|
||||||
|
|
@ -272,6 +280,23 @@ namespace spot
|
||||||
throw std::runtime_error
|
throw std::runtime_error
|
||||||
(std::string("unknown option for print_dot(): ") + c);
|
(std::string("unknown option for print_dot(): ") + c);
|
||||||
}
|
}
|
||||||
|
if (opt_html_labels_)
|
||||||
|
{
|
||||||
|
nl_ = "<br/>";
|
||||||
|
label_pre_ = "label=<";
|
||||||
|
label_post_ = '>';
|
||||||
|
}
|
||||||
|
if (opt_latex_)
|
||||||
|
{
|
||||||
|
if (opt_latex_ && opt_html_labels_)
|
||||||
|
throw std::runtime_error
|
||||||
|
(std::string("print_dot(): options 'r' and 'R' "
|
||||||
|
"are incompatible with 'x'"));
|
||||||
|
nl_ = "\\\\{}";
|
||||||
|
label_pre_ = "texlbl=\"";
|
||||||
|
label_post_ = '"';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dotty_output(std::ostream& os, const char* options)
|
dotty_output(std::ostream& os, const char* options)
|
||||||
|
|
@ -306,7 +331,12 @@ namespace spot
|
||||||
output_set(acc_cond::mark_t a) const
|
output_set(acc_cond::mark_t a) const
|
||||||
{
|
{
|
||||||
if (!opt_all_bullets)
|
if (!opt_all_bullets)
|
||||||
os_ << '{';
|
{
|
||||||
|
if (opt_latex_)
|
||||||
|
os_ << "\\{";
|
||||||
|
else
|
||||||
|
os_ << '{';
|
||||||
|
}
|
||||||
const char* space = "";
|
const char* space = "";
|
||||||
for (auto v: a.sets())
|
for (auto v: a.sets())
|
||||||
{
|
{
|
||||||
|
|
@ -316,7 +346,12 @@ namespace spot
|
||||||
space = ",";
|
space = ",";
|
||||||
}
|
}
|
||||||
if (!opt_all_bullets)
|
if (!opt_all_bullets)
|
||||||
os_ << '}';
|
{
|
||||||
|
if (opt_latex_)
|
||||||
|
os_ << "\\}";
|
||||||
|
else
|
||||||
|
os_ << '}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char*
|
const char*
|
||||||
|
|
@ -369,8 +404,30 @@ namespace spot
|
||||||
os_ << '}';
|
os_ << '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::ostream&
|
||||||
state_label(unsigned s) const
|
escape_for_output(std::ostream& os, const std::string& s) const
|
||||||
|
{
|
||||||
|
if (opt_html_labels_)
|
||||||
|
return escape_html(os, s);
|
||||||
|
if (opt_latex_)
|
||||||
|
return escape_latex(os, s);
|
||||||
|
return escape_str(os, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream&
|
||||||
|
format_label(std::ostream& os, bdd label) const
|
||||||
|
{
|
||||||
|
formula f = bdd_to_formula(label, aut_->get_dict());
|
||||||
|
if (opt_latex_)
|
||||||
|
{
|
||||||
|
print_sclatex_psl(os << '$', f) << '$';
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
return escape_for_output(os, str_psl(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream&
|
||||||
|
format_state_label(std::ostream& os, unsigned s) const
|
||||||
{
|
{
|
||||||
bdd label = bddfalse;
|
bdd label = bddfalse;
|
||||||
for (auto& t: aut_->out(s))
|
for (auto& t: aut_->out(s))
|
||||||
|
|
@ -380,8 +437,8 @@ namespace spot
|
||||||
}
|
}
|
||||||
if (label == bddfalse
|
if (label == bddfalse
|
||||||
&& incomplete_ && incomplete_->find(s) != incomplete_->end())
|
&& incomplete_ && incomplete_->find(s) != incomplete_->end())
|
||||||
return "...";
|
return os << "...";
|
||||||
return bdd_format_formula(aut_->get_dict(), label);
|
return format_label(os, label);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string string_dst(int dst, int color_num = -1)
|
std::string string_dst(int dst, int color_num = -1)
|
||||||
|
|
@ -420,26 +477,27 @@ namespace spot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void print_acceptance_for_human()
|
std::string get_acceptance_for_human()
|
||||||
{
|
{
|
||||||
const char* nl = opt_html_labels_ ? "<br/>" : "\\n";
|
std::ostringstream os;
|
||||||
|
|
||||||
if (aut_->acc().is_generalized_buchi())
|
if (aut_->acc().is_generalized_buchi())
|
||||||
{
|
{
|
||||||
if (aut_->acc().is_all())
|
if (aut_->acc().is_all())
|
||||||
os_ << nl << "[all]";
|
os << "all";
|
||||||
else if (aut_->acc().is_buchi())
|
else if (aut_->acc().is_buchi())
|
||||||
os_ << nl << "[Büchi]";
|
os << "Büchi";
|
||||||
else
|
else
|
||||||
os_ << nl << "[gen. Büchi " << aut_->num_sets() << ']';
|
os << "gen. Büchi " << aut_->num_sets();
|
||||||
}
|
}
|
||||||
else if (aut_->acc().is_generalized_co_buchi())
|
else if (aut_->acc().is_generalized_co_buchi())
|
||||||
{
|
{
|
||||||
if (aut_->acc().is_none())
|
if (aut_->acc().is_none())
|
||||||
os_ << nl << "[none]";
|
os << "none";
|
||||||
else if (aut_->acc().is_co_buchi())
|
else if (aut_->acc().is_co_buchi())
|
||||||
os_ << nl << "[co-Büchi]";
|
os << "co-Büchi";
|
||||||
else
|
else
|
||||||
os_ << nl << "[gen. co-Büchi " << aut_->num_sets() << ']';
|
os << "gen. co-Büchi " << aut_->num_sets();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -447,7 +505,7 @@ namespace spot
|
||||||
assert(r != 0);
|
assert(r != 0);
|
||||||
if (r > 0)
|
if (r > 0)
|
||||||
{
|
{
|
||||||
os_ << nl << "[Rabin " << r << ']';
|
os << "Rabin " << r;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -455,14 +513,14 @@ namespace spot
|
||||||
assert(r != 0);
|
assert(r != 0);
|
||||||
if (r > 0)
|
if (r > 0)
|
||||||
{
|
{
|
||||||
os_ << nl << "[Streett " << r << ']';
|
os << "Streett " << r;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::vector<unsigned> pairs;
|
std::vector<unsigned> pairs;
|
||||||
if (aut_->acc().is_generalized_rabin(pairs))
|
if (aut_->acc().is_generalized_rabin(pairs))
|
||||||
{
|
{
|
||||||
os_ << nl << "[gen. Rabin " << pairs.size() << ']';
|
os << "gen. Rabin " << pairs.size();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -470,10 +528,10 @@ namespace spot
|
||||||
bool odd = false;
|
bool odd = false;
|
||||||
if (aut_->acc().is_parity(max, odd))
|
if (aut_->acc().is_parity(max, odd))
|
||||||
{
|
{
|
||||||
os_ << nl << "[parity "
|
os << "parity "
|
||||||
<< (max ? "max " : "min ")
|
<< (max ? "max " : "min ")
|
||||||
<< (odd ? "odd " : "even ")
|
<< (odd ? "odd " : "even ")
|
||||||
<< aut_->num_sets() << ']';
|
<< aut_->num_sets();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -484,14 +542,23 @@ namespace spot
|
||||||
bool s_like = aut_->acc().is_streett_like(s_pairs);
|
bool s_like = aut_->acc().is_streett_like(s_pairs);
|
||||||
unsigned ssz = s_pairs.size();
|
unsigned ssz = s_pairs.size();
|
||||||
if (r_like && (!s_like || (rsz <= ssz)))
|
if (r_like && (!s_like || (rsz <= ssz)))
|
||||||
os_ << nl << "[Rabin-like " << rsz << ']';
|
os << "Rabin-like " << rsz;
|
||||||
else if (s_like && (!r_like || (ssz < rsz)))
|
else if (s_like && (!r_like || (ssz < rsz)))
|
||||||
os_ << nl << "[Streett-like " << ssz << ']';
|
os << "Streett-like " << ssz;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return os.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_acceptance_for_human()
|
||||||
|
{
|
||||||
|
std::string accstr = get_acceptance_for_human();
|
||||||
|
if (accstr.empty())
|
||||||
|
return;
|
||||||
|
os_ << nl_ << '[' << accstr << ']';
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -503,56 +570,39 @@ namespace spot
|
||||||
if (opt_bullet && aut_->num_sets() <= MAX_BULLET)
|
if (opt_bullet && aut_->num_sets() <= MAX_BULLET)
|
||||||
opt_all_bullets = true;
|
opt_all_bullets = true;
|
||||||
os_ << "digraph G {\n";
|
os_ << "digraph G {\n";
|
||||||
|
if (opt_latex_)
|
||||||
|
os_ << " d2tgraphstyle=\"every node/.style={align=center}\"\n";
|
||||||
if (!opt_vertical_)
|
if (!opt_vertical_)
|
||||||
os_ << " rankdir=LR\n";
|
os_ << " rankdir=LR\n";
|
||||||
if (name_ || opt_show_acc_)
|
if (name_ || opt_show_acc_)
|
||||||
{
|
{
|
||||||
if (!opt_html_labels_)
|
os_ << " " << label_pre_;
|
||||||
|
if (name_)
|
||||||
|
escape_for_output(os_, *name_);
|
||||||
|
if (opt_show_acc_)
|
||||||
{
|
{
|
||||||
os_ << " label=\"";
|
if (!dcircles_)
|
||||||
if (name_)
|
|
||||||
{
|
{
|
||||||
escape_str(os_, *name_);
|
if (name_)
|
||||||
if (opt_show_acc_)
|
os_ << nl_;
|
||||||
os_ << "\\n";
|
auto& acc = aut_->get_acceptance();
|
||||||
|
if (opt_html_labels_)
|
||||||
|
acc.to_html(os_, [this](std::ostream& os, int v)
|
||||||
|
{
|
||||||
|
this->output_html_set_aux(os, v);
|
||||||
|
});
|
||||||
|
else if (opt_latex_)
|
||||||
|
acc.to_latex(os_ << '$') << '$';
|
||||||
|
else
|
||||||
|
acc.to_text(os_, [this](std::ostream& os, int v)
|
||||||
|
{
|
||||||
|
this->output_set(os, v);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
if (opt_show_acc_)
|
print_acceptance_for_human();
|
||||||
{
|
|
||||||
if (!dcircles_)
|
|
||||||
{
|
|
||||||
aut_->get_acceptance().to_text
|
|
||||||
(os_, [this](std::ostream& os, int v)
|
|
||||||
{
|
|
||||||
this->output_set(os, v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
print_acceptance_for_human();
|
|
||||||
}
|
|
||||||
os_ << "\"\n";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
os_ << " label=<";
|
|
||||||
if (name_)
|
|
||||||
{
|
|
||||||
escape_html(os_, *name_);
|
|
||||||
if (opt_show_acc_)
|
|
||||||
os_ << "<br/>";
|
|
||||||
}
|
|
||||||
if (opt_show_acc_)
|
|
||||||
{
|
|
||||||
if (!dcircles_)
|
|
||||||
{
|
|
||||||
aut_->get_acceptance().to_html
|
|
||||||
(os_, [this](std::ostream& os, int v)
|
|
||||||
{
|
|
||||||
this->output_html_set_aux(os, v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
print_acceptance_for_human();
|
|
||||||
}
|
|
||||||
os_ << ">\n";
|
|
||||||
}
|
}
|
||||||
|
os_ << label_post_ << '\n';
|
||||||
os_ << " labelloc=\"t\"\n";
|
os_ << " labelloc=\"t\"\n";
|
||||||
}
|
}
|
||||||
switch (opt_shape_)
|
switch (opt_shape_)
|
||||||
|
|
@ -607,6 +657,16 @@ namespace spot
|
||||||
void
|
void
|
||||||
process_state(unsigned s)
|
process_state(unsigned s)
|
||||||
{
|
{
|
||||||
|
os_ << " " << s << " [" << label_pre_;
|
||||||
|
if (sn_ && s < sn_->size() && !(*sn_)[s].empty())
|
||||||
|
escape_for_output(os_, (*sn_)[s]);
|
||||||
|
else if (sprod_)
|
||||||
|
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
|
||||||
|
else
|
||||||
|
os_ << s;
|
||||||
|
if (orig_ && s < orig_->size())
|
||||||
|
os_ << " (" << (*orig_)[s] << ')';
|
||||||
|
|
||||||
if (mark_states_ && !dcircles_)
|
if (mark_states_ && !dcircles_)
|
||||||
{
|
{
|
||||||
acc_cond::mark_t acc = 0U;
|
acc_cond::mark_t acc = 0U;
|
||||||
|
|
@ -616,64 +676,23 @@ namespace spot
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_name = sn_ && s < sn_->size() && !(*sn_)[s].empty();
|
if (acc)
|
||||||
|
|
||||||
os_ << " " << s << " [label=";
|
|
||||||
if (!opt_html_labels_)
|
|
||||||
{
|
{
|
||||||
os_ << '"';
|
os_ << nl_;
|
||||||
if (has_name)
|
if (opt_html_labels_)
|
||||||
escape_str(os_, (*sn_)[s]);
|
output_html_set(acc);
|
||||||
else if (sprod_)
|
|
||||||
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
|
|
||||||
else
|
else
|
||||||
os_ << s;
|
output_set(acc);
|
||||||
if (orig_ && s < orig_->size())
|
|
||||||
os_ << " (" << (*orig_)[s] << ')';
|
|
||||||
if (acc)
|
|
||||||
{
|
|
||||||
os_ << "\\n";
|
|
||||||
output_set(acc);
|
|
||||||
}
|
|
||||||
if (opt_state_labels_)
|
|
||||||
escape_str(os_ << "\\n", state_label(s));
|
|
||||||
os_ << '"';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
os_ << '<';
|
|
||||||
if (has_name)
|
|
||||||
escape_html(os_, (*sn_)[s]);
|
|
||||||
else if (sprod_)
|
|
||||||
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
|
|
||||||
else
|
|
||||||
os_ << s;
|
|
||||||
if (orig_ && s < orig_->size())
|
|
||||||
os_ << " (" << (*orig_)[s] << ')';
|
|
||||||
if (acc)
|
|
||||||
{
|
|
||||||
os_ << "<br/>";
|
|
||||||
output_html_set(acc);
|
|
||||||
}
|
|
||||||
if (opt_state_labels_)
|
|
||||||
escape_html(os_ << "<br/>", state_label(s));
|
|
||||||
os_ << '>';
|
|
||||||
}
|
}
|
||||||
|
if (opt_state_labels_)
|
||||||
|
format_state_label(os_ << nl_, s);
|
||||||
|
os_ << label_post_;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
os_ << " " << s << " [label=\"";
|
|
||||||
if (sn_ && s < sn_->size() && !(*sn_)[s].empty())
|
|
||||||
escape_str(os_, (*sn_)[s]);
|
|
||||||
else if (sprod_)
|
|
||||||
os_ << (*sprod_)[s].first << ',' << (*sprod_)[s].second;
|
|
||||||
else
|
|
||||||
os_ << s;
|
|
||||||
if (orig_ && s < orig_->size())
|
|
||||||
os_ << " (" << (*orig_)[s] << ')';
|
|
||||||
if (opt_state_labels_)
|
if (opt_state_labels_)
|
||||||
escape_str(os_ << "\\n", state_label(s));
|
format_state_label(os_ << nl_, s);
|
||||||
os_ << '"';
|
os_ << label_post_;
|
||||||
// Use state_acc_sets(), not state_is_accepting() because
|
// Use state_acc_sets(), not state_is_accepting() because
|
||||||
// on co-Büchi automata we want to mark the rejecting
|
// on co-Büchi automata we want to mark the rejecting
|
||||||
// states.
|
// states.
|
||||||
|
|
@ -713,35 +732,20 @@ namespace spot
|
||||||
if (iter != highlight_edges_->end())
|
if (iter != highlight_edges_->end())
|
||||||
os_ << '.' << iter->second % palette_mod;
|
os_ << '.' << iter->second % palette_mod;
|
||||||
}
|
}
|
||||||
std::string label;
|
os_ << " [" << label_pre_;
|
||||||
if (!opt_state_labels_)
|
if (!opt_state_labels_)
|
||||||
label = bdd_format_formula(aut_->get_dict(), t.cond);
|
format_label(os_, t.cond);
|
||||||
if (!opt_html_labels_)
|
if (!mark_states_)
|
||||||
{
|
if (auto a = t.acc)
|
||||||
os_ << " [label=\"";
|
{
|
||||||
escape_str(os_, label);
|
if (!opt_state_labels_)
|
||||||
if (!mark_states_)
|
os_ << nl_;
|
||||||
if (auto a = t.acc)
|
if (opt_html_labels_)
|
||||||
{
|
output_html_set(a);
|
||||||
if (!opt_state_labels_)
|
else
|
||||||
os_ << "\\n";
|
output_set(a);
|
||||||
output_set(a);
|
}
|
||||||
}
|
os_ << label_post_;
|
||||||
os_ << '"';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
os_ << " [label=<";
|
|
||||||
escape_html(os_, label);
|
|
||||||
if (!mark_states_)
|
|
||||||
if (auto a = t.acc)
|
|
||||||
{
|
|
||||||
if (!opt_state_labels_)
|
|
||||||
os_ << "<br/>";
|
|
||||||
output_html_set(a);
|
|
||||||
}
|
|
||||||
os_ << '>';
|
|
||||||
}
|
|
||||||
if (opt_ordered_edges_ || opt_numbered_edges_)
|
if (opt_ordered_edges_ || opt_numbered_edges_)
|
||||||
{
|
{
|
||||||
os_ << ", taillabel=\"";
|
os_ << ", taillabel=\"";
|
||||||
|
|
@ -886,7 +890,7 @@ namespace spot
|
||||||
{
|
{
|
||||||
// Reset the label, otherwise the graph label would
|
// Reset the label, otherwise the graph label would
|
||||||
// be inherited by the cluster.
|
// be inherited by the cluster.
|
||||||
os_ << " label=\"\"\n";
|
os_ << (opt_latex_ ? " texlbl=\"\"\n" : " label=\"\"\n");
|
||||||
}
|
}
|
||||||
for (auto s: si->states_of(i))
|
for (auto s: si->states_of(i))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ TESTS_twa = \
|
||||||
core/gragsa.test \
|
core/gragsa.test \
|
||||||
core/dstar.test \
|
core/dstar.test \
|
||||||
core/readsave.test \
|
core/readsave.test \
|
||||||
|
core/dot2tex.test \
|
||||||
core/ltldo.test \
|
core/ltldo.test \
|
||||||
core/ltldo2.test \
|
core/ltldo2.test \
|
||||||
core/maskacc.test \
|
core/maskacc.test \
|
||||||
|
|
|
||||||
|
|
@ -573,27 +573,27 @@ digraph G {
|
||||||
subgraph cluster_0 {
|
subgraph cluster_0 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
4 [label="t"]
|
4 [label=<t>]
|
||||||
}
|
}
|
||||||
subgraph cluster_1 {
|
subgraph cluster_1 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
1 [label="G(a & b)"]
|
1 [label=<G(a & b)>]
|
||||||
}
|
}
|
||||||
subgraph cluster_2 {
|
subgraph cluster_2 {
|
||||||
color=red
|
color=red
|
||||||
label=""
|
label=""
|
||||||
2 [label="F!a"]
|
2 [label=<F!a>]
|
||||||
}
|
}
|
||||||
subgraph cluster_3 {
|
subgraph cluster_3 {
|
||||||
color=red
|
color=red
|
||||||
label=""
|
label=""
|
||||||
3 [label="F!b"]
|
3 [label=<F!b>]
|
||||||
}
|
}
|
||||||
subgraph cluster_4 {
|
subgraph cluster_4 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
0 [label="c R (c | G(a & b) | (F!b & F!a))"]
|
0 [label=<c R (c | G(a & b) | (F!b & F!a))>]
|
||||||
-1 [label=<>,shape=point]
|
-1 [label=<>,shape=point]
|
||||||
-4 [label=<>,shape=point]
|
-4 [label=<>,shape=point]
|
||||||
-7 [label=<>,shape=point]
|
-7 [label=<>,shape=point]
|
||||||
|
|
@ -669,26 +669,26 @@ digraph G {
|
||||||
subgraph cluster_0 {
|
subgraph cluster_0 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
4 [label="t"]
|
4 [label=<t>]
|
||||||
}
|
}
|
||||||
subgraph cluster_1 {
|
subgraph cluster_1 {
|
||||||
color=red
|
color=red
|
||||||
label=""
|
label=""
|
||||||
2 [label="F!a"]
|
2 [label=<F!a>]
|
||||||
}
|
}
|
||||||
subgraph cluster_2 {
|
subgraph cluster_2 {
|
||||||
color=red
|
color=red
|
||||||
label=""
|
label=""
|
||||||
3 [label="F!b"]
|
3 [label=<F!b>]
|
||||||
}
|
}
|
||||||
subgraph cluster_3 {
|
subgraph cluster_3 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
0 [label="c R (c | G(a & b) | (F!b & F!a))"]
|
0 [label=<c R (c | G(a & b) | (F!b & F!a))>]
|
||||||
-1 [label=<>,shape=point]
|
-1 [label=<>,shape=point]
|
||||||
-4 [label=<>,shape=point]
|
-4 [label=<>,shape=point]
|
||||||
-7 [label=<>,shape=point]
|
-7 [label=<>,shape=point]
|
||||||
1 [label="G(a & b)"]
|
1 [label=<G(a & b)>]
|
||||||
-10 [label=<>,shape=point]
|
-10 [label=<>,shape=point]
|
||||||
}
|
}
|
||||||
0 -> 4 [label=<c>]
|
0 -> 4 [label=<c>]
|
||||||
|
|
@ -852,22 +852,22 @@ digraph G {
|
||||||
subgraph cluster_0 {
|
subgraph cluster_0 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
3 [label="t"]
|
3 [label=<t>]
|
||||||
}
|
}
|
||||||
subgraph cluster_1 {
|
subgraph cluster_1 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
1 [label="Fa"]
|
1 [label=<Fa>]
|
||||||
}
|
}
|
||||||
subgraph cluster_2 {
|
subgraph cluster_2 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
2 [label="G!a"]
|
2 [label=<G!a>]
|
||||||
}
|
}
|
||||||
subgraph cluster_3 {
|
subgraph cluster_3 {
|
||||||
color=green
|
color=green
|
||||||
label=""
|
label=""
|
||||||
0 [label="G((b & Fa) | (!b & G!a))"]
|
0 [label=<G((b & Fa) | (!b & G!a))>]
|
||||||
-1 [label=<>,shape=point]
|
-1 [label=<>,shape=point]
|
||||||
-4 [label=<>,shape=point]
|
-4 [label=<>,shape=point]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
tests/core/dot2tex.test
Executable file
40
tests/core/dot2tex.test
Executable file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2017 Laboratoire de Recherche et Développement
|
||||||
|
# de l'Epita (LRDE).
|
||||||
|
#
|
||||||
|
# This file is part of Spot, a model checking library.
|
||||||
|
#
|
||||||
|
# Spot is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Spot is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||||
|
# License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
. ./defs
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# This tests our support for LaTeX-embedded dot, for dot2tex.
|
||||||
|
|
||||||
|
# Skip this test if we don't find all the tools we need.
|
||||||
|
(latexmk --version) || exit 77
|
||||||
|
(pdflatex --version) || exit 77
|
||||||
|
(dot2tex --version) || exit 77
|
||||||
|
(dot -V) || exit 77
|
||||||
|
|
||||||
|
ltl2tgba 'a U b' --dot=scanx >out.dot
|
||||||
|
dot2tex --autosize --nominsize out.dot >out.tex
|
||||||
|
pdflatex out.tex
|
||||||
|
|
||||||
|
ltl2tgba 'p0 U p1' --dot=tax >out2.dot
|
||||||
|
dot2tex --autosize --nominsize out2.dot >out2.tex
|
||||||
|
grep -F 'p_{0}' out2.tex
|
||||||
|
grep -F 'mathsf{Inf}' out2.tex
|
||||||
|
pdflatex out2.tex
|
||||||
|
|
@ -406,7 +406,7 @@ digraph G {
|
||||||
edge [fontname="Lato"]
|
edge [fontname="Lato"]
|
||||||
I [label="", style=invis, width=0]
|
I [label="", style=invis, width=0]
|
||||||
I -> 0
|
I -> 0
|
||||||
0 [label="0"]
|
0 [label=<0>]
|
||||||
0 -> 0 [label=<a & b<br/>$zero$one>]
|
0 -> 0 [label=<a & b<br/>$zero$one>]
|
||||||
0 -> 0 [label=<!a & !b>]
|
0 -> 0 [label=<!a & !b>]
|
||||||
0 -> 0 [label=<!a & b<br/>$one>]
|
0 -> 0 [label=<!a & b<br/>$one>]
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,9 @@
|
||||||
" size=\"10.2,5\" edge[arrowhead=vee, arrowsize=.7]\n",
|
" size=\"10.2,5\" edge[arrowhead=vee, arrowsize=.7]\n",
|
||||||
" I [label=\"\", style=invis, width=0]\n",
|
" I [label=\"\", style=invis, width=0]\n",
|
||||||
" I -> 1\n",
|
" I -> 1\n",
|
||||||
" 0 [label=\"0\", peripheries=2]\n",
|
" 0 [label=<0>, peripheries=2]\n",
|
||||||
" 0 -> 0 [label=<1>]\n",
|
" 0 -> 0 [label=<1>]\n",
|
||||||
" 1 [label=\"1\"]\n",
|
" 1 [label=<1>]\n",
|
||||||
" 1 -> 0 [label=<b>]\n",
|
" 1 -> 0 [label=<b>]\n",
|
||||||
" 1 -> 1 [label=<a & !b>]\n",
|
" 1 -> 1 [label=<a & !b>]\n",
|
||||||
"}\n",
|
"}\n",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue