From 52e268be5782e6133e88cfd1f34bd45a635c2c47 Mon Sep 17 00:00:00 2001 From: Chris White Date: Mon, 31 Aug 2020 21:49:55 -0400 Subject: [PATCH 1/8] Always use the C++ compiler for generated C code The registry relies on C++, so use it throughout. --- rules.mk | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rules.mk b/rules.mk index 8dd0bb4..e26dca3 100644 --- a/rules.mk +++ b/rules.mk @@ -60,12 +60,16 @@ MY_VALA_PKGS = \ --pkg gio-2.0 \ $(EOL) -# Vala settings. LOCAL_VALA_FLAGS is filled in by each Makefile.am with -# any other valac options that Makefile.am needs. -# TODO remove USER_VALAFLAGS once I figure out why regular VALAFLAGS -# isn't being passed through. +# Vala settings. +# - LOCAL_VALA_FLAGS is filled in by each Makefile.am with any other valac +# options that Makefile.am needs. +# - Always use the C++ compiler for the generated code, since the +# registry relies on it. +# - TODO remove USER_VALAFLAGS once I figure out why regular VALAFLAGS +# isn't being passed through. AM_VALAFLAGS = \ $(LOCAL_VALA_FLAGS) \ + --cc=$(CXX) \ $(MY_VALA_PKGS) \ $(USER_VALAFLAGS) \ $(EOL) From 808c6d19c7b1a07356b68242dee2ef32868e24df Mon Sep 17 00:00:00 2001 From: Chris White Date: Mon, 7 Sep 2020 17:37:07 -0400 Subject: [PATCH 2/8] Add misc/ for support files not part of this project For example, right now it has a regex-tester CLI. --- misc/.gitignore | 1 + misc/regex-tester.vala | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 misc/.gitignore create mode 100644 misc/regex-tester.vala diff --git a/misc/.gitignore b/misc/.gitignore new file mode 100644 index 0000000..3128145 --- /dev/null +++ b/misc/.gitignore @@ -0,0 +1 @@ +regex-tester diff --git a/misc/regex-tester.vala b/misc/regex-tester.vala new file mode 100644 index 0000000..b029c4d --- /dev/null +++ b/misc/regex-tester.vala @@ -0,0 +1,37 @@ + +static int main(string[] args) +{ + Regex re; + if(args.length < 3) { + return 2; + } + + try { + re = new Regex(args[1]); + } catch(RegexError e) { // LCOV_EXCL_START + printerr("Could not create regex qr{%s}\n", args[0]); + return 1; + } + + print(@"Regex: qr{$(re.get_pattern())}\n"); + for(int argidx = 2; argidx{ + s.append("REPL"); + return false; + }); + print("after: -%s-\n", after); + + return 0; +} From 34178657a6072be39e5126a5325ce34e9c4981a9 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sat, 29 Aug 2020 21:00:52 -0400 Subject: [PATCH 3/8] Update documentation, manpage; bump version --- configure.ac | 2 +- src/Makefile.am | 8 +++- src/core/registry-impl.cpp | 3 +- src/core/registry.vala | 5 +++ src/logging/logging.vala | 8 ++++ src/md4c | 2 +- src/pfft.pod | 77 ++++++++++++++++++++++++++++++++++---- 7 files changed, 92 insertions(+), 13 deletions(-) diff --git a/configure.ac b/configure.ac index 2061de1..e43846e 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ dnl === Basic setup ======================================================= -AC_INIT([pfft markdown-to-PDF converter], [0.0.1], [], [pfft], [https://github.com/cxw42/pfft]) +AC_INIT([pfft markdown-to-PDF converter], [0.0.2], [], [pfft], [https://github.com/cxw42/pfft]) AC_PREREQ([2.65]) AC_COPYRIGHT([Copyright (C) 2020 Christopher White]) AC_CONFIG_SRCDIR([rules.mk]) dnl make sure the srcdir is correctly specified diff --git a/src/Makefile.am b/src/Makefile.am index 6b7a3c1..2c04b72 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,5 +19,9 @@ dist_man1_MANS = pfft.1 EXTRA_DIST += pfft.pod -pfft.1: pfft.pod - $(AM_V_GEN)pod2man $< $@ +pfft.1: pfft.pod Makefile.am + $(AM_V_GEN)pod2man $< $@ \ + -s 1 \ + --center='User Commands' \ + -r 'v@PACKAGE_VERSION@' \ + $(EOL) diff --git a/src/core/registry-impl.cpp b/src/core/registry-impl.cpp index 55f8924..cf008cc 100644 --- a/src/core/registry-impl.cpp +++ b/src/core/registry-impl.cpp @@ -16,7 +16,8 @@ GHashTable *my_get_registry() } void my_register_type(const gchar *name, GType ty, - const gchar *filename, const guint lineno) + G_GNUC_UNUSED const gchar *filename, + G_GNUC_UNUSED const guint lineno) { GHashTable *registry = my_get_registry(); g_hash_table_insert(registry, (gpointer)name, (gpointer)ty); diff --git a/src/core/registry.vala b/src/core/registry.vala index 619caf3..bf7c5a0 100644 --- a/src/core/registry.vala +++ b/src/core/registry.vala @@ -29,6 +29,11 @@ namespace My { /** * Convenience function for registering a type. + * @param name The handle of the type. This can be different from any + * names the type may have in Vala or GLib. + * @param type The type's GType + * @param filename Only used for debugging + * @param lineno Only used for debugging */ [CCode(cheader_filename = "registry.h")] public extern void register_type(string name, GLib.Type type, diff --git a/src/logging/logging.vala b/src/logging/logging.vala index 5db0bb1..425cc14 100644 --- a/src/logging/logging.vala +++ b/src/logging/logging.vala @@ -102,8 +102,16 @@ namespace My { [CCode (cheader_filename = "logging-c.h")] public extern bool lenabled(Gst.DebugLevel level); + /** + * A copy of g_canonicalize_filename, which was added to Glib after + * Ubuntu Bionic. + * + * This doesn't belong in a logging library, but since this is the LGPL + * part of pfft, here it is! + */ [CCode (cheader_filename = "logging-c.h")] public extern string canonicalize_filename (string filename, string? relative_to = null); + } } diff --git a/src/md4c b/src/md4c index 3980028..06f5acd 160000 --- a/src/md4c +++ b/src/md4c @@ -1 +1 @@ -Subproject commit 3980028ad8d05217d4bc4cb243a805809081592a +Subproject commit 06f5acd11febdc8c1e9c4dc320210629256b0287 diff --git a/src/pfft.pod b/src/pfft.pod index 3277ce8..c94f747 100644 --- a/src/pfft.pod +++ b/src/pfft.pod @@ -6,27 +6,73 @@ pfft - PDF From Formatted Text =head1 SYNOPSIS -TODO + pfft [OPTION...] FILENAME... - produce a PDF from each FILENAME -=head1 DESCRIPTION - -TODO +Processes Markdown file FILENAME and outputs a PDF. +Visit https://github.com/cxw42/pfft for more information. =head1 OPTIONS -TODO +=head2 Help Options + +=over + +=item -h, --help + +Show help options + +=back + +=head2 Application Options + +=over + +=item -V, --version + +Display version number + +=item -v, --verbose + +Verbosity (can be given multiple times) + +=item -R, --reader=READER + +Which reader to use + +=item --ro=NAME=VALUE + +Set a reader option + +=item -o, --output=FILENAME + +Output filename (provided only one input filename is given) + +=item -W, --writer=WRITER + +Which writer to use + +=item --wo=NAME=VALUE + +Set a writer option + +=back =head1 ENVIRONMENT -TODO +No enviroment variables are used at present. =head1 FILES -TODO +The default output filename is the same as the input filename, but with the +extension changed to C<.pdf>. + +If you only specify one input filename on the command line, you can give the +C<-o> option to set the output filename. =head1 EXAMPLES -TODO + $ pfft foo.md # produces foo.pdf + $ GST_DEBUG='pfft:9' pfft -v foo.md # _lots_ of debug output! =head1 AUTHOR @@ -40,6 +86,8 @@ Please use the GitHub bug tracker at L. Copyright (c) 2020, Christopher White. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -75,6 +123,19 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +=head2 LGPL portions + +Files in the C directory in the source distribution are +licensed LGPL 2.1 or later (SPDX-License-Identifier: LGPL-2.1-or-later). +They include functions that are: +Copyright (c) 2020 Christopher White; +Copyright (C) 1999,2000 Erik Walthinsen ; +Copyright 2000 Red Hat, Inc.; or +Copyright (C) 1994-2018 Free Software Foundation, Inc. + +The full source of those portions is available to be copied from +L. + =head1 SEE ALSO pandoc(1), pdflatex(1) --- much more capable, but much heavier. From 1ec4b2d087bd3cc659b7596c1d3c95c980aca698 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sat, 29 Aug 2020 21:47:43 -0400 Subject: [PATCH 4/8] Added --quiet; switched more logging to Gst - Add -q/--quiet option, which beats GST_DEBUG and --verbose - Adjust the GStreamer log level for our debug category based on -q and -v - Switch more logging from print() to GStreamer logging controlled by -q, -v --- src/pfft.pod | 20 +++++++++++-- src/pfft.vala | 56 ++++++++++++++++++++++-------------- src/reader/md4c-reader.vala | 10 +++---- src/writer/pango-markup.vala | 3 +- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/pfft.pod b/src/pfft.pod index c94f747..6480188 100644 --- a/src/pfft.pod +++ b/src/pfft.pod @@ -33,7 +33,12 @@ Display version number =item -v, --verbose -Verbosity (can be given multiple times) +Verbosity. Can be given up to five times, each time increasing verbosity. + +=item -q, --quiet + +Turn off output. If both C<-q> and C<-v> are given, the C<-q> wins, and +messages are not printed. =item -R, --reader=READER @@ -59,7 +64,18 @@ Set a writer option =head1 ENVIRONMENT -No enviroment variables are used at present. +=over + +=item C + +This controls the verbosity. Set + + GST_DEBUG='pfft:X' + +to set verbosity to level C. C means no messages (like C<-q>), and +C means everything (like giving C<-v> five or more times). + +=back =head1 FILES diff --git a/src/pfft.vala b/src/pfft.vala index 0a576b0..b5ee806 100644 --- a/src/pfft.vala +++ b/src/pfft.vala @@ -46,6 +46,9 @@ namespace My { */ private static int opt_verbose = 0; + /** Quietness, which beats verbosity */ + private bool opt_quiet = false; + /** * Input filename(s). * @@ -92,6 +95,8 @@ namespace My { // --verbose { "verbose", 'v', OptionFlags.NO_ARG, OptionArg.CALLBACK, (void *)cb_verbose, "Verbosity (can be given multiple times)", null }, + // --quiet + { "quiet", 'q', 0, OptionArg.NONE, &opt_quiet, "Silence debugging output (cancels --verbose)", null }, // --reader, -R READER { "reader", 'R', 0, OptionArg.STRING, &opt_reader_name, "Which reader to use", "READER" }, @@ -110,20 +115,6 @@ namespace My { // FILENAME* (non-option arg(s) - inputs) { OPTION_REMAINING, 0, 0, OptionArg.FILENAME_ARRAY, &opt_infns, "Filename(s) to process", "FILENAME..." }, - /* - // --driver - { "driver", 0, 0, OptionArg.STRING, ref driver, "Use the given driver", "DRIVER" }, - // [--import STRING]* - { "import", 0, 0, OptionArg.STRING_ARRAY, ref import_packages, "Include binding for PACKAGE", "PACKAGE..." }, - - // --double DOUBLE - { "double", 0, 0, OptionArg.DOUBLE, ref numd, "double value", "DOUBLE" }, - // --int64 INT64 - { "int64", 0, 0, OptionArg.INT64, ref numi64, "int64 value", "INT64" }, - // --int INT - { "int", 0, 0, OptionArg.INT, ref numi, "int value", "INT" }, - */ - // list terminator { null } }; @@ -172,6 +163,9 @@ namespace My { return 1; } + // Convert verbosity into GST_DEBUG levels + set_verbosity(); + if (opt_version) { print("%s\nVisit %s for more information\n", PACKAGE_STRING, PACKAGE_URL); return 0; @@ -219,9 +213,7 @@ namespace My { return 1; } - if(opt_verbose > 0) { - print("Using reader %s, writer %s\n", reader_name, writer_name); - } + linfo("Using reader %s, writer %s", reader_name, writer_name); /* Do the work */ for(uint i=0; i 0) { - print("Processing %s to %s\n", infh.get_path(), outfn); - } + linfo("Processing %s to %s", infh.get_path(), outfn); var doc = reader.read_document(infh.get_path()); writer.write_document(outfn, doc, infh.get_path()); } // process_file() + void set_verbosity() + { + if(opt_quiet) { + Log.category.set_threshold(NONE); + opt_verbose = 0; + return; + } + + int oldlevel = Log.category.get_threshold(); + int newlevel = int.max(oldlevel, Gst.DebugLevel.ERROR); + + if(opt_verbose == 1) { + newlevel = int.max(newlevel, Gst.DebugLevel.INFO); + } else if(opt_verbose == 2) { + newlevel = int.max(newlevel, Gst.DebugLevel.DEBUG); + } else if(opt_verbose == 3) { + newlevel = int.max(newlevel, Gst.DebugLevel.LOG); + } else if(opt_verbose == 4) { + newlevel = int.max(newlevel, Gst.DebugLevel.TRACE); + } else if(opt_verbose > 0) { + newlevel = int.max(newlevel, Gst.DebugLevel.MEMDUMP); + } + Log.category.set_threshold((Gst.DebugLevel)newlevel); + } // }}}1 // Registry functions {{{1 diff --git a/src/reader/md4c-reader.vala b/src/reader/md4c-reader.vala index 10a7d26..cfdd572 100644 --- a/src/reader/md4c-reader.vala +++ b/src/reader/md4c-reader.vala @@ -71,7 +71,7 @@ namespace My GLib.Node newnode = null; var self = (MarkdownMd4cReader)userdata; - print("%sGot block %s\n", + llog("%sGot block %s", self.indent_, block_type.to_string()); ++self.depth_; @@ -136,7 +136,7 @@ namespace My { var self = (MarkdownMd4cReader)userdata; --self.depth_; - print("%sLeaving block %s\n", + llog("%sLeaving block %s", self.indent_, block_type.to_string()); // Pop out of the last span, if we're in one @@ -158,7 +158,7 @@ namespace My GLib.Node newnode = null; var self = (MarkdownMd4cReader)userdata; - print("%sGot span %s ... ", + llog("%sGot span %s ... ", self.indent_, span_type.to_string()); switch(span_type) { @@ -195,7 +195,7 @@ namespace My private static int leave_span_(SpanType span_type, void *detail, void *userdata) { var self = (MarkdownMd4cReader)userdata; - print("left span %s\n", span_type.to_string()); + llog("left span %s", span_type.to_string()); // Move back into the parent span self.node_ = self.node_.parent; @@ -214,7 +214,7 @@ namespace My { var self = (MarkdownMd4cReader)userdata; var data = strndup((char *)text, size); - print("%s<<%s>>\n", self.indent_, data); + llog("%s<<%s>>", self.indent_, data); var newnode = node_of_ty(SPAN_PLAIN); newnode.data.text = data; diff --git a/src/writer/pango-markup.vala b/src/writer/pango-markup.vala index 2eef1fe..dd55dbb 100644 --- a/src/writer/pango-markup.vala +++ b/src/writer/pango-markup.vala @@ -390,8 +390,7 @@ namespace My { var state = state_in; - // DEBUG - print("process_node_into: %s%s %p = '%s'\n", + ldebug("process_node_into: %s%s %p = '%s'", string.nfill(node.depth()*4, ' '), el.ty.to_string(), node, text_markup); From 5d21eae05ebf6f24af0acc349d531ddbcf90dbad Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 30 Aug 2020 20:47:28 -0400 Subject: [PATCH 5/8] Added code-coverage testing Local ===== - Add new code-coverage macros, as suggested by - Add targets - Gitignore coverage-data files CI == - Add codecov.io to Travis builds; fix Bionic builds - Report coverage to codecov.io - Yet more hacks in src/logging to permit Travis builds to pass on Bionic's default valac 0.40. --- .gitignore | 7 + .travis.yml | 11 +- README.md | 9 +- configure.ac | 27 ++++ m4/ax_ac_append_to_file.m4 | 32 +++++ m4/ax_ac_print_to_file.m4 | 32 +++++ m4/ax_add_am_macro_static.m4 | 28 ++++ m4/ax_am_macros_static.m4 | 38 +++++ m4/ax_check_gnu_make.m4 | 95 ++++++++++++ m4/ax_code_coverage.m4 | 272 +++++++++++++++++++++++++++++++++++ m4/ax_file_escapes.m4 | 30 ++++ rules.mk | 17 ++- src/logging/Makefile.am | 14 +- 13 files changed, 605 insertions(+), 7 deletions(-) create mode 100644 m4/ax_ac_append_to_file.m4 create mode 100644 m4/ax_ac_print_to_file.m4 create mode 100644 m4/ax_add_am_macro_static.m4 create mode 100644 m4/ax_am_macros_static.m4 create mode 100644 m4/ax_check_gnu_make.m4 create mode 100644 m4/ax_code_coverage.m4 create mode 100644 m4/ax_file_escapes.m4 diff --git a/.gitignore b/.gitignore index c718d82..45e9221 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,12 @@ pfft *.idb *.pdb +# Coverage +*.gcda +*.gcno +/*coverage.info +/*coverage/ + # Kernel Module Compile Results *.mod* *.cmd @@ -63,6 +69,7 @@ dkms.conf # autotools /aclocal.m4 +/aminclude_static.am /autom4te.cache/ /build-aux/ conf*.dir/ diff --git a/.travis.yml b/.travis.yml index 3c4efec..1357a98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ addons: - valadoc - graphviz-dev - help2man + - lcov - autotools-dev - libpango1.0-dev - libsnapd-glib-dev @@ -28,11 +29,19 @@ before_script: script: - ./configure --disable-dependency-tracking - # The dependency tracking doesn't work on Travis for some reason --- + # The dependency tracking doesn't work on Travis for some reason --- # e.g, https://travis-ci.org/github/cxw42/pfft/builds/718187356 - make V=1 - make test V=1 after_failure: + - find . -name pfft-logging.h -exec sh -c 'echo "$0" ; cat "$0"' {} ';' - ls -alR - find . -name Makefile -exec sh -c 'echo "$0" ; cat "$0"' {} ';' + +after_success: + - | + make -j maintainer-clean ; ./bootstrap && \ + ./configure --disable-dependency-tracking --enable-code-coverage USER_VALAFLAGS='-g' CFLAGS='-g -O0' && \ + make -j4 check-code-coverage && \ + bash <(curl -s https://codecov.io/bash) ; : -f pfft-coverage.info diff --git a/README.md b/README.md index 02567e5..2832a52 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Install Vala: Install development dependencies for pfft: - $ sudo apt install -y libpango1.0-dev libgee-0.8-dev libgstreamer1.0-dev autotools-dev uncrustify perl + $ sudo apt install -y libpango1.0-dev libgee-0.8-dev libgstreamer1.0-dev autotools-dev uncrustify perl lcov Initialize submodules: @@ -64,6 +64,13 @@ Note: `libpango1.0-dev` pulls in Pango, Cairo, and pangocairo. In GLib 2.62+, the default output format is TAP. Therefore, you can do `make build-tests && prove`. +### Checking code coverage + + ./configure --enable-code-coverage && make -j4 check-code-coverage + +This will print a summary to the console. For the full report, open +`pfft--coverage/index.html` in a Web browser. + ### Making a release $ make distcheck diff --git a/configure.ac b/configure.ac index e43846e..57a6548 100644 --- a/configure.ac +++ b/configure.ac @@ -24,6 +24,33 @@ AC_ARG_VAR([USER_VALAFLAGS], [extra options for valac(1)]) AC_PROG_RANLIB +dnl === Code coverage ===================================================== + +dnl For some reason, the coverage data is referring to src/code/glib-2.0.vapi. +dnl Inject code to strip that from the .info file so genhtml can succeed. +dnl This is all very ugly. For example, the variables in this section are +dnl hardwired for use in a Makefile, since they assume Makefile syntax +dnl (embedded in sh(1) escaping). + +AC_CHECK_PROG([GENHTMLREAL], [genhtml], [genhtml]) + +GENHTMLHACK="\$(GENHTMLREAL)" +AC_CHECK_PROG([GENHTML], [genhtml], [\$(GENHTMLHACK)]) + +AX_AM_MACROS_STATIC +AX_CODE_COVERAGE + +AM_COND_IF( + [CODE_COVERAGE_ENABLED], + [ dnl then + AC_SUBST([GENHTMLHACK], ['dnl + perl -n -i -e '"'"'print unless m{src/core/glib-2.0.vapi}..m{^end_of_record}'"'"' "$(CODE_COVERAGE_OUTPUT_FILE)" ; dnl + LANG=C $(GENHTMLREAL) dnl + ']) + ] +) + + dnl === Sanity checks ===================================================== AC_MSG_CHECKING([for local md4c]) diff --git a/m4/ax_ac_append_to_file.m4 b/m4/ax_ac_append_to_file.m4 new file mode 100644 index 0000000..242b3d5 --- /dev/null +++ b/m4/ax_ac_append_to_file.m4 @@ -0,0 +1,32 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_ac_append_to_file.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AC_APPEND_TO_FILE([FILE],[DATA]) +# +# DESCRIPTION +# +# Appends the specified data to the specified Autoconf is run. If you want +# to append to a file when configure is run use AX_APPEND_TO_FILE instead. +# +# LICENSE +# +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +AC_DEFUN([AX_AC_APPEND_TO_FILE],[ +AC_REQUIRE([AX_FILE_ESCAPES]) +m4_esyscmd( +AX_FILE_ESCAPES +[ +printf "%s" "$2" >> "$1" +]) +]) diff --git a/m4/ax_ac_print_to_file.m4 b/m4/ax_ac_print_to_file.m4 new file mode 100644 index 0000000..642dfc1 --- /dev/null +++ b/m4/ax_ac_print_to_file.m4 @@ -0,0 +1,32 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_ac_print_to_file.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AC_PRINT_TO_FILE([FILE],[DATA]) +# +# DESCRIPTION +# +# Writes the specified data to the specified file when Autoconf is run. If +# you want to print to a file when configure is run use AX_PRINT_TO_FILE +# instead. +# +# LICENSE +# +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +AC_DEFUN([AX_AC_PRINT_TO_FILE],[ +m4_esyscmd( +AC_REQUIRE([AX_FILE_ESCAPES]) +[ +printf "%s" "$2" > "$1" +]) +]) diff --git a/m4/ax_add_am_macro_static.m4 b/m4/ax_add_am_macro_static.m4 new file mode 100644 index 0000000..6442d24 --- /dev/null +++ b/m4/ax_add_am_macro_static.m4 @@ -0,0 +1,28 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_add_am_macro_static.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_ADD_AM_MACRO_STATIC([RULE]) +# +# DESCRIPTION +# +# Adds the specified rule to $AMINCLUDE. +# +# LICENSE +# +# Copyright (c) 2009 Tom Howard +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AC_DEFUN([AX_ADD_AM_MACRO_STATIC],[ + AC_REQUIRE([AX_AM_MACROS_STATIC]) + AX_AC_APPEND_TO_FILE(AMINCLUDE_STATIC,[$1]) +]) diff --git a/m4/ax_am_macros_static.m4 b/m4/ax_am_macros_static.m4 new file mode 100644 index 0000000..f4cee8c --- /dev/null +++ b/m4/ax_am_macros_static.m4 @@ -0,0 +1,38 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_am_macros_static.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AM_MACROS_STATIC +# +# DESCRIPTION +# +# Adds support for macros that create Automake rules. You must manually +# add the following line +# +# include $(top_srcdir)/aminclude_static.am +# +# to your Makefile.am files. +# +# LICENSE +# +# Copyright (c) 2009 Tom Howard +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +AC_DEFUN([AMINCLUDE_STATIC],[aminclude_static.am]) + +AC_DEFUN([AX_AM_MACROS_STATIC], +[ +AX_AC_PRINT_TO_FILE(AMINCLUDE_STATIC,[ +# ]AMINCLUDE_STATIC[ generated automatically by Autoconf +# from AX_AM_MACROS_STATIC on ]m4_esyscmd([LC_ALL=C date])[ +]) +]) diff --git a/m4/ax_check_gnu_make.m4 b/m4/ax_check_gnu_make.m4 new file mode 100644 index 0000000..785dc96 --- /dev/null +++ b/m4/ax_check_gnu_make.m4 @@ -0,0 +1,95 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_gnu_make.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_GNU_MAKE([run-if-true],[run-if-false]) +# +# DESCRIPTION +# +# This macro searches for a GNU version of make. If a match is found: +# +# * The makefile variable `ifGNUmake' is set to the empty string, otherwise +# it is set to "#". This is useful for including a special features in a +# Makefile, which cannot be handled by other versions of make. +# * The makefile variable `ifnGNUmake' is set to #, otherwise +# it is set to the empty string. This is useful for including a special +# features in a Makefile, which can be handled +# by other versions of make or to specify else like clause. +# * The variable `_cv_gnu_make_command` is set to the command to invoke +# GNU make if it exists, the empty string otherwise. +# * The variable `ax_cv_gnu_make_command` is set to the command to invoke +# GNU make by copying `_cv_gnu_make_command`, otherwise it is unset. +# * If GNU Make is found, its version is extracted from the output of +# `make --version` as the last field of a record of space-separated +# columns and saved into the variable `ax_check_gnu_make_version`. +# * Additionally if GNU Make is found, run shell code run-if-true +# else run shell code run-if-false. +# +# Here is an example of its use: +# +# Makefile.in might contain: +# +# # A failsafe way of putting a dependency rule into a makefile +# $(DEPEND): +# $(CC) -MM $(srcdir)/*.c > $(DEPEND) +# +# @ifGNUmake@ ifeq ($(DEPEND),$(wildcard $(DEPEND))) +# @ifGNUmake@ include $(DEPEND) +# @ifGNUmake@ else +# fallback code +# @ifGNUmake@ endif +# +# Then configure.in would normally contain: +# +# AX_CHECK_GNU_MAKE() +# AC_OUTPUT(Makefile) +# +# Then perhaps to cause gnu make to override any other make, we could do +# something like this (note that GNU make always looks for GNUmakefile +# first): +# +# if ! test x$_cv_gnu_make_command = x ; then +# mv Makefile GNUmakefile +# echo .DEFAULT: > Makefile ; +# echo \ $_cv_gnu_make_command \$@ >> Makefile; +# fi +# +# Then, if any (well almost any) other make is called, and GNU make also +# exists, then the other make wraps the GNU make. +# +# LICENSE +# +# Copyright (c) 2008 John Darrington +# Copyright (c) 2015 Enrico M. Crisostomo +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 12 + +AC_DEFUN([AX_CHECK_GNU_MAKE],dnl + [AC_PROG_AWK + AC_CACHE_CHECK([for GNU make],[_cv_gnu_make_command],[dnl + _cv_gnu_make_command="" ; +dnl Search all the common names for GNU make + for a in "$MAKE" make gmake gnumake ; do + if test -z "$a" ; then continue ; fi ; + if "$a" --version 2> /dev/null | grep GNU 2>&1 > /dev/null ; then + _cv_gnu_make_command=$a ; + AX_CHECK_GNU_MAKE_HEADLINE=$("$a" --version 2> /dev/null | grep "GNU Make") + ax_check_gnu_make_version=$(echo ${AX_CHECK_GNU_MAKE_HEADLINE} | ${AWK} -F " " '{ print $(NF); }') + break ; + fi + done ;]) +dnl If there was a GNU version, then set @ifGNUmake@ to the empty string, '#' otherwise + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifGNUmake], ["#"])], [AS_VAR_SET([ifGNUmake], [""])]) + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifnGNUmake], [""])], [AS_VAR_SET([ifnGNUmake], ["#"])]) + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_UNSET(ax_cv_gnu_make_command)], [AS_VAR_SET([ax_cv_gnu_make_command], [${_cv_gnu_make_command}])]) + AS_VAR_IF([_cv_gnu_make_command], [""],[$2],[$1]) + AC_SUBST([ifGNUmake]) + AC_SUBST([ifnGNUmake]) +]) diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4 new file mode 100644 index 0000000..352165b --- /dev/null +++ b/m4/ax_code_coverage.m4 @@ -0,0 +1,272 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CODE_COVERAGE() +# +# DESCRIPTION +# +# Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS, +# CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included +# in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every +# build target (program or library) which should be built with code +# coverage support. Also add rules using AX_ADD_AM_MACRO_STATIC; and +# $enable_code_coverage which can be used in subsequent configure output. +# CODE_COVERAGE_ENABLED is defined and substituted, and corresponds to the +# value of the --enable-code-coverage option, which defaults to being +# disabled. +# +# Test also for gcov program and create GCOV variable that could be +# substituted. +# +# Note that all optimization flags in CFLAGS must be disabled when code +# coverage is enabled. +# +# Usage example: +# +# configure.ac: +# +# AX_CODE_COVERAGE +# +# Makefile.am: +# +# include $(top_srcdir)/aminclude_static.am +# +# my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ... +# my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ... +# my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ... +# my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ... +# +# clean-local: code-coverage-clean +# distclean-local: code-coverage-dist-clean +# +# This results in a "check-code-coverage" rule being added to any +# Makefile.am which do "include $(top_srcdir)/aminclude_static.am" +# (assuming the module has been configured with --enable-code-coverage). +# Running `make check-code-coverage` in that directory will run the +# module's test suite (`make check`) and build a code coverage report +# detailing the code which was touched, then print the URI for the report. +# +# This code was derived from Makefile.decl in GLib, originally licensed +# under LGPLv2.1+. +# +# LICENSE +# +# Copyright (c) 2012, 2016 Philip Withnall +# Copyright (c) 2012 Xan Lopez +# Copyright (c) 2012 Christian Persch +# Copyright (c) 2012 Paolo Borelli +# Copyright (c) 2012 Dan Winship +# Copyright (c) 2015,2018 Bastien ROUCARIES +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or (at +# your option) any later version. +# +# This library 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 Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +#serial 34 + +m4_define(_AX_CODE_COVERAGE_RULES,[ +AX_ADD_AM_MACRO_STATIC([ +# Code coverage +# +# Optional: +# - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting. +# Multiple directories may be specified, separated by whitespace. +# (Default: \$(top_builddir)) +# - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated +# by lcov for code coverage. (Default: +# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info) +# - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage +# reports to be created. (Default: +# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage) +# - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage, +# set to 0 to disable it and leave empty to stay with the default. +# (Default: empty) +# - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov +# instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) +# - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov +# instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) +# - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov +# - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the +# collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) +# - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov +# instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) +# - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering +# lcov instance. (Default: empty) +# - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov +# instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) +# - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the +# genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) +# - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml +# instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +# - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore +# +# The generated report will be titled using the \$(PACKAGE_NAME) and +# \$(PACKAGE_VERSION). In order to add the current git hash to the title, +# use the git-version-gen script, available online. +# Optional variables +# run only on top dir +if CODE_COVERAGE_ENABLED + ifeq (\$(abs_builddir), \$(abs_top_builddir)) +CODE_COVERAGE_DIRECTORY ?= \$(top_builddir) +CODE_COVERAGE_OUTPUT_FILE ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info +CODE_COVERAGE_OUTPUT_DIRECTORY ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage + +CODE_COVERAGE_BRANCH_COVERAGE ?= +CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= \$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\ +--rc lcov_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE)) +CODE_COVERAGE_LCOV_SHOPTS ?= \$(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) +CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool \"\$(GCOV)\" +CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= \$(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) +CODE_COVERAGE_LCOV_OPTIONS ?= \$(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) +CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?= +CODE_COVERAGE_LCOV_RMOPTS ?= \$(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) +CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\ +\$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\ +--rc genhtml_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE)) +CODE_COVERAGE_GENHTML_OPTIONS ?= \$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +CODE_COVERAGE_IGNORE_PATTERN ?= + +GITIGNOREFILES := \$(GITIGNOREFILES) \$(CODE_COVERAGE_OUTPUT_FILE) \$(CODE_COVERAGE_OUTPUT_DIRECTORY) +code_coverage_v_lcov_cap = \$(code_coverage_v_lcov_cap_\$(V)) +code_coverage_v_lcov_cap_ = \$(code_coverage_v_lcov_cap_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_lcov_cap_0 = @echo \" LCOV --capture\" \$(CODE_COVERAGE_OUTPUT_FILE); +code_coverage_v_lcov_ign = \$(code_coverage_v_lcov_ign_\$(V)) +code_coverage_v_lcov_ign_ = \$(code_coverage_v_lcov_ign_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_lcov_ign_0 = @echo \" LCOV --remove /tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN); +code_coverage_v_genhtml = \$(code_coverage_v_genhtml_\$(V)) +code_coverage_v_genhtml_ = \$(code_coverage_v_genhtml_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_genhtml_0 = @echo \" GEN \" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\"; +code_coverage_quiet = \$(code_coverage_quiet_\$(V)) +code_coverage_quiet_ = \$(code_coverage_quiet_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_quiet_0 = --quiet + +# sanitizes the test-name: replaces with underscores: dashes and dots +code_coverage_sanitize = \$(subst -,_,\$(subst .,_,\$(1))) + +# Use recursive makes in order to ignore errors during check +check-code-coverage: + -\$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) -k check + \$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) code-coverage-capture + +# Capture code coverage data +code-coverage-capture: code-coverage-capture-hook + \$(code_coverage_v_lcov_cap)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --capture --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" --test-name \"\$(call code_coverage_sanitize,\$(PACKAGE_NAME)-\$(PACKAGE_VERSION))\" --no-checksum --compat-libtool \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_OPTIONS) + \$(code_coverage_v_lcov_ign)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --remove \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"/tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN) --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_RMOPTS) + -@rm -f \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" + \$(code_coverage_v_genhtml)LANG=C \$(GENHTML) \$(code_coverage_quiet) \$(addprefix --prefix ,\$(CODE_COVERAGE_DIRECTORY)) --output-directory \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" --title \"\$(PACKAGE_NAME)-\$(PACKAGE_VERSION) Code Coverage\" --legend --show-details \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_GENHTML_OPTIONS) + @echo \"file://\$(abs_builddir)/\$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html\" + +code-coverage-clean: + -\$(LCOV) --directory \$(top_builddir) -z + -rm -rf \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" + -find . \\( -name \"*.gcda\" -o -name \"*.gcno\" -o -name \"*.gcov\" \\) -delete + +code-coverage-dist-clean: + +A][M_DISTCHECK_CONFIGURE_FLAGS := \$(A][M_DISTCHECK_CONFIGURE_FLAGS) --disable-code-coverage + else # ifneq (\$(abs_builddir), \$(abs_top_builddir)) +check-code-coverage: + +code-coverage-capture: code-coverage-capture-hook + +code-coverage-clean: + +code-coverage-dist-clean: + endif # ifeq (\$(abs_builddir), \$(abs_top_builddir)) +else #! CODE_COVERAGE_ENABLED +# Use recursive makes in order to ignore errors during check +check-code-coverage: + @echo \"Need to reconfigure with --enable-code-coverage\" +# Capture code coverage data +code-coverage-capture: code-coverage-capture-hook + @echo \"Need to reconfigure with --enable-code-coverage\" + +code-coverage-clean: + +code-coverage-dist-clean: + +endif #CODE_COVERAGE_ENABLED +# Hook rule executed before code-coverage-capture, overridable by the user +code-coverage-capture-hook: + +.PHONY: check-code-coverage code-coverage-capture code-coverage-dist-clean code-coverage-clean code-coverage-capture-hook +]) +]) + +AC_DEFUN([_AX_CODE_COVERAGE_ENABLED],[ + AX_CHECK_GNU_MAKE([],[AC_MSG_ERROR([not using GNU make that is needed for coverage])]) + AC_REQUIRE([AX_ADD_AM_MACRO_STATIC]) + # check for gcov + AC_CHECK_TOOL([GCOV], + [$_AX_CODE_COVERAGE_GCOV_PROG_WITH], + [:]) + AS_IF([test "X$GCOV" = "X:"], + [AC_MSG_ERROR([gcov is needed to do coverage])]) + AC_SUBST([GCOV]) + + dnl Check if gcc is being used + AS_IF([ test "$GCC" = "no" ], [ + AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage]) + ]) + + AC_CHECK_PROG([LCOV], [lcov], [lcov]) + AC_CHECK_PROG([GENHTML], [genhtml], [genhtml]) + + AS_IF([ test x"$LCOV" = x ], [ + AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed]) + ]) + + AS_IF([ test x"$GENHTML" = x ], [ + AC_MSG_ERROR([Could not find genhtml from the lcov package]) + ]) + + dnl Build the code coverage flags + dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility + CODE_COVERAGE_CPPFLAGS="-DNDEBUG" + CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" + CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" + CODE_COVERAGE_LIBS="-lgcov" + + AC_SUBST([CODE_COVERAGE_CPPFLAGS]) + AC_SUBST([CODE_COVERAGE_CFLAGS]) + AC_SUBST([CODE_COVERAGE_CXXFLAGS]) + AC_SUBST([CODE_COVERAGE_LIBS]) +]) + +AC_DEFUN([AX_CODE_COVERAGE],[ + dnl Check for --enable-code-coverage + + # allow to override gcov location + AC_ARG_WITH([gcov], + [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])], + [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov], + [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov]) + + AC_MSG_CHECKING([whether to build with code coverage support]) + AC_ARG_ENABLE([code-coverage], + AS_HELP_STRING([--enable-code-coverage], + [Whether to enable code coverage support]),, + enable_code_coverage=no) + + AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test "x$enable_code_coverage" = xyes]) + AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage]) + AC_MSG_RESULT($enable_code_coverage) + + AS_IF([ test "x$enable_code_coverage" = xyes ], [ + _AX_CODE_COVERAGE_ENABLED + ]) + + _AX_CODE_COVERAGE_RULES +]) diff --git a/m4/ax_file_escapes.m4 b/m4/ax_file_escapes.m4 new file mode 100644 index 0000000..a86fdc3 --- /dev/null +++ b/m4/ax_file_escapes.m4 @@ -0,0 +1,30 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_FILE_ESCAPES +# +# DESCRIPTION +# +# Writes the specified data to the specified file. +# +# LICENSE +# +# Copyright (c) 2008 Tom Howard +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AC_DEFUN([AX_FILE_ESCAPES],[ +AX_DOLLAR="\$" +AX_SRB="\\135" +AX_SLB="\\133" +AX_BS="\\\\" +AX_DQ="\"" +]) diff --git a/rules.mk b/rules.mk index e26dca3..bbf3b0f 100644 --- a/rules.mk +++ b/rules.mk @@ -79,9 +79,10 @@ AM_VALAFLAGS = \ # C settings, which are the same throughout. LOCAL_CFLAGS is filled in # by each Makefile.am. -AM_CFLAGS = $(LOCAL_CFLAGS) $(INPUT_CFLAGS) $(RENDER_CFLAGS) $(BASE_CFLAGS) -AM_CXXFLAGS = $(AM_CFLAGS) -LIBS = $(INPUT_LIBS) $(RENDER_LIBS) $(BASE_LIBS) +AM_CFLAGS = $(LOCAL_CFLAGS) $(INPUT_CFLAGS) $(RENDER_CFLAGS) $(BASE_CFLAGS) $(CODE_COVERAGE_CFLAGS) +AM_CXXFLAGS = $(AM_CFLAGS) $(CODE_COVERAGE_CXXFLAGS) +AM_CPPFLAGS = $(CODE_COVERAGE_CPPFLAGS) +LIBS = $(INPUT_LIBS) $(RENDER_LIBS) $(BASE_LIBS) $(CODE_COVERAGE_LIBS) # Flags used by both the program and the tests --- anything that links # against all the libraries @@ -102,3 +103,13 @@ MY_use_all_ldadd = \ $(top_builddir)/src/$(dir)/libpfft-$(dir).a \ ) \ $(EOL) + +# For code coverage, per +# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html +clean-local: code-coverage-clean +distclean-local: code-coverage-dist-clean + +CODE_COVERAGE_OUTPUT_FILE = $(PACKAGE_TARNAME)-coverage.info +CODE_COVERAGE_OUTPUT_DIRECTORY = $(PACKAGE_TARNAME)-coverage + +include $(top_srcdir)/aminclude_static.am diff --git a/src/logging/Makefile.am b/src/logging/Makefile.am index 7bc0be4..1cd5744 100644 --- a/src/logging/Makefile.am +++ b/src/logging/Makefile.am @@ -1,19 +1,29 @@ include $(top_srcdir)/rules.mk +hdrstamp = tweaked-header.stamp + # All the .vala files are run through valac at once. LOCAL_VALA_FLAGS = -H pfft-logging.h --library pfft-logging --vapi pfft-logging.vapi EXTRA_DIST = pfft-logging.h pfft-logging.vapi noinst_LIBRARIES = libpfft-logging.a -libpfft_logging_a_SOURCES = $(MY_logging_VALA) $(MY_logging_EXTRASOURCES) +libpfft_logging_a_SOURCES = $(MY_logging_VALA) $(MY_logging_EXTRASOURCES) \ + $(hdrstamp) # Run after valac to create logging.o by hand. logging.c has nothing we need, # and on older Vala versions includes function prototypes of the GST_DEBUG # macros! Therefore, create an empty logging.o from an empty C file so that # the link command will work. -logging.$(OBJEXT): $(srcdir)/libpfft_logging_a_vala.stamp +logging.$(OBJEXT): $(srcdir)/libpfft_logging_a_vala.stamp $(hdrstamp) cat /dev/null > dummy.c $(CC) -c -o $@ dummy.c CLEANFILES = dummy.c + +# Remove GST_* function prototypes from pfft-logging.h +$(hdrstamp): $(srcdir)/libpfft_logging_a_vala.stamp + perl -i -e 'local $$/; $$_ = <>; s/^void GST.*?;//gms; s/^gboolean my_log_lenabled.*?;//gms; print' $(srcdir)/pfft-logging.h + touch $@ + +MAINTAINERCLEANFILES = $(hdrstamp) From 076d7fca4d3f146f8c7dddcba562d1d7729d2ee7 Mon Sep 17 00:00:00 2001 From: Chris White Date: Mon, 31 Aug 2020 18:27:30 -0400 Subject: [PATCH 6/8] Add tests of core, pango-markup, etc.; add diag() - tests: automatically grab source file name Rename t/*.vala and update t/Makefile.am so Automake can automatically determine the Vala source file for each test program. This way we don't need a `*_SOURCES` line for each test program. - Split 001-basic into 001-sanity and 200-md4c-reader - Begin adding pango-markup writer tests, among others - Mark some uncoverable lines - core/util: add diag() for printing diagnostics during tests. --- configure.ac | 4 +- src/core/util.vala | 37 +++++ src/core/writer.vala | 2 +- src/writer/pango-markup.vala | 8 +- t/.gitignore | 2 +- t/001-sanity-t.vala | 28 ++++ t/010-core-util-t.vala | 26 ++++ t/020-registry-t.vala | 38 +++++ t/050-core-el-t.vala | 44 ++++++ t/060-both-headleft.pfft | 11 ++ t/060-no-meta.pfft | 2 + t/070-core-writer-t.vala | 39 +++++ t/100-logging-t.vala | 36 +++++ t/{001-basic.vala => 200-md4c-reader-t.vala} | 30 +--- t/300-pango-markup-writer-t.vala | 145 +++++++++++++++++++ t/Makefile.am | 25 +++- t/{001-basic.md => basic.md} | 0 17 files changed, 443 insertions(+), 34 deletions(-) create mode 100644 t/001-sanity-t.vala create mode 100644 t/010-core-util-t.vala create mode 100644 t/020-registry-t.vala create mode 100644 t/050-core-el-t.vala create mode 100644 t/060-both-headleft.pfft create mode 100644 t/060-no-meta.pfft create mode 100644 t/070-core-writer-t.vala create mode 100644 t/100-logging-t.vala rename t/{001-basic.vala => 200-md4c-reader-t.vala} (67%) create mode 100644 t/300-pango-markup-writer-t.vala rename t/{001-basic.md => basic.md} (100%) diff --git a/configure.ac b/configure.ac index 57a6548..96220f2 100644 --- a/configure.ac +++ b/configure.ac @@ -26,7 +26,7 @@ AC_PROG_RANLIB dnl === Code coverage ===================================================== -dnl For some reason, the coverage data is referring to src/code/glib-2.0.vapi. +dnl For some reason, the coverage data is referring to src/.../glib-2.0.vapi. dnl Inject code to strip that from the .info file so genhtml can succeed. dnl This is all very ugly. For example, the variables in this section are dnl hardwired for use in a Makefile, since they assume Makefile syntax @@ -44,7 +44,7 @@ AM_COND_IF( [CODE_COVERAGE_ENABLED], [ dnl then AC_SUBST([GENHTMLHACK], ['dnl - perl -n -i -e '"'"'print unless m{src/core/glib-2.0.vapi}..m{^end_of_record}'"'"' "$(CODE_COVERAGE_OUTPUT_FILE)" ; dnl + perl -n -i -e '"'"'print unless m{\b(?:src|t)/?.*?/glib-2.0.vapi}..m{^end_of_record}'"'"' "$(CODE_COVERAGE_OUTPUT_FILE)" ; dnl LANG=C $(GENHTMLREAL) dnl ']) ] diff --git a/src/core/util.vala b/src/core/util.vala index 348a658..a8bcf8e 100644 --- a/src/core/util.vala +++ b/src/core/util.vala @@ -35,4 +35,41 @@ namespace My { return new GLib.Node(new Elem(newty)); } + /** + * Wrap a string in TAP markers. + * + * NOTE: discards trailing whitespace. + */ + private string tap_wrap(string s) + { + var s1 = s; + s1._chomp(); + var retval = "# " + s1.replace("\n", "\n# ") + "\n"; + return retval; + } + + /** Format a string as a TAP diagnostic message. */ + [PrintfFormat] + public string diag_string (string format, ...) + { + var l = va_list(); + var raw = format.vprintf(l); + var retval = tap_wrap(raw); + return retval; + } // diag_string() + + /** + * Print a TAP diagnostic message to stdout. + * + * For use in test files in t/. + */ + [PrintfFormat] + public void diag (string format, ...) + { + var l = va_list(); + var raw = format.vprintf(l); + var retval = tap_wrap(raw); + print("%s", retval); + } // diag() + } // My diff --git a/src/core/writer.vala b/src/core/writer.vala index 28ef854..45aeb93 100644 --- a/src/core/writer.vala +++ b/src/core/writer.vala @@ -24,7 +24,7 @@ namespace My { /** * Convenience function to map filename "-" to stdout */ - public void emit(string filename, string contents) + public static void emit(string filename, string contents) throws FileError { if(filename == "-") { diff --git a/src/writer/pango-markup.vala b/src/writer/pango-markup.vala index dd55dbb..c58c208 100644 --- a/src/writer/pango-markup.vala +++ b/src/writer/pango-markup.vala @@ -132,10 +132,10 @@ namespace My { try { re_newline = new Regex("\\R+"); re_command = new Regex("^pfft:\\s*(\\w+)"); - } catch(RegexError e) { + } catch(RegexError e) { // LCOV_EXCL_START lerroro(this, "Could not create required regexes --- I can't go on"); assert(false); // die horribly --- something is very wrong! - } + } // LCOV_EXCL_STOP } /** @@ -184,7 +184,7 @@ namespace My { while(true) { // Render this block, which may take more than one pass // Parameters to render() are page-relative if(surf.status() != Cairo.Status.SUCCESS) { - lerroro(blk, "Surface status: %s", surf.status().to_string()); + lerroro(blk, "Surface status: %s", surf.status().to_string()); // LCOV_EXCL_LINE because I can't force this to happen } var ok = blk.render(cr, rightP, bottomP); @@ -209,8 +209,10 @@ namespace My { // Save the PDF surf.finish(); if(surf.status() != Cairo.Status.SUCCESS) { + // LCOV_EXCL_START because I can't force this to happen throw new Error.WRITER("Could not save PDF: " + surf.status().to_string()); + // LCOV_EXCL_STOP } } // write_document() diff --git a/t/.gitignore b/t/.gitignore index 8954050..f3f980c 100644 --- a/t/.gitignore +++ b/t/.gitignore @@ -2,7 +2,7 @@ *.log # Test executables -*.t +*-t # Generated files [0-9]*.c diff --git a/t/001-sanity-t.vala b/t/001-sanity-t.vala new file mode 100644 index 0000000..ae451c9 --- /dev/null +++ b/t/001-sanity-t.vala @@ -0,0 +1,28 @@ +// 001-sanity-t.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; +/** + * argv[0], for use by sanity() + */ +private string program_name; + +void test_sanity() +{ + Test.message("%s: Running sanity test in %s() at %s:%d", + program_name, GLib.Log.METHOD, GLib.Log.FILE, GLib.Log.LINE); + assert_true(true); +} + +public static int main (string[] args) +{ + program_name = args[0]; + + // run the tests + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/001-sanity/sanity", test_sanity); + + return Test.run(); +} diff --git a/t/010-core-util-t.vala b/t/010-core-util-t.vala new file mode 100644 index 0000000..d838370 --- /dev/null +++ b/t/010-core-util-t.vala @@ -0,0 +1,26 @@ +// t/010-core-util.vala - tests of src/core/util.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +void test_diag() +{ + string s; + + s = diag_string("Hello, world!"); + assert_true(s == "# Hello, world!\n"); + s = diag_string("1\n2"); + assert_true(s == "# 1\n# 2\n"); + s = diag_string("1\n2\n"); // strips trailing whitespace + assert_true(s == "# 1\n# 2\n"); +} + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/010-core-util/diag", test_diag); + + return Test.run(); +} diff --git a/t/020-registry-t.vala b/t/020-registry-t.vala new file mode 100644 index 0000000..9d1faee --- /dev/null +++ b/t/020-registry-t.vala @@ -0,0 +1,38 @@ +// 020-registry.vala - tests of pfft plugin registration +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +class TestClass : Object +{ + /** Metadata for this class */ + [Description(blurb = "Sample")] + public bool meta { get; default = false; } +} + +void test_get_registry() +{ + assert_nonnull(get_registry()); +} + +void test_register() +{ + register_type("testclass", typeof(TestClass), GLib.Log.FILE, GLib.Log.LINE); + assert_true(true); + var registry = get_registry(); + assert_nonnull(registry); + var ty = registry.get("testclass"); + assert_true(ty == typeof(TestClass)); +} + + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/020-registry/get_registry", test_get_registry); + Test.add_func("/020-registry/register", test_register); + + return Test.run(); +} diff --git a/t/050-core-el-t.vala b/t/050-core-el-t.vala new file mode 100644 index 0000000..596b6ee --- /dev/null +++ b/t/050-core-el-t.vala @@ -0,0 +1,44 @@ +// 050-core-el.vala - tests of src/core/el.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +private Elem new_elem() +{ + var el = new Elem(BLOCK_HEADER); + el.text = "t"; + el.header_level = 1; + el.info_string = "i"; + el.href = "h"; + + return el; +} + +void test_clone() +{ + var el = new_elem(); + var el2 = el.clone(); + assert_true(el2.text == el.text); + assert_true(el2.header_level == el.header_level); + assert_true(el2.info_string == el.info_string); + assert_true(el2.href == el.href); +} + +void test_as_string() +{ + var el = new_elem(); + assert_true(el.as_string() == "MY_ELEM_TYPE_BLOCK_HEADER/i: -t-"); + el.info_string = ""; + assert_true(el.as_string() == "MY_ELEM_TYPE_BLOCK_HEADER: -t-"); +} + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/050-core-el/clone", test_clone); + Test.add_func("/050-core-el/as_string", test_as_string); + + return Test.run(); +} diff --git a/t/060-both-headleft.pfft b/t/060-both-headleft.pfft new file mode 100644 index 0000000..1d99b85 --- /dev/null +++ b/t/060-both-headleft.pfft @@ -0,0 +1,11 @@ +# t/060-both-headleft.pfft +# A test with both header.left and header.leftmarkup (throws) + +[pfft] +version = 1 + +[header] +left = Hl +leftmarkup = Hl< + +# vi: set ft=desktop: # diff --git a/t/060-no-meta.pfft b/t/060-no-meta.pfft new file mode 100644 index 0000000..5f71215 --- /dev/null +++ b/t/060-no-meta.pfft @@ -0,0 +1,2 @@ +[group] +key = value diff --git a/t/070-core-writer-t.vala b/t/070-core-writer-t.vala new file mode 100644 index 0000000..aea7007 --- /dev/null +++ b/t/070-core-writer-t.vala @@ -0,0 +1,39 @@ +// t/070-core-writer-t.vala - tests of src/core/writer.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +private const string MSG = "Hello, world!\n"; + +void test_emit_file() +{ + try { + string destfn, contents; + FileUtils.close(FileUtils.open_tmp("pfft-t-XXXXXX", out destfn)); + Writer.emit(destfn, MSG); + FileUtils.get_contents(destfn, out contents); + assert_true(contents == MSG); + + try { + var destf = File.new_for_path(destfn); + destf.delete(); + } catch(GLib.Error e) { + //ignore errors + } + + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_not_reached(); + } +} + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/060-core-writer/emit_file", test_emit_file); + // TODO also test the write-to-stdout code path + + return Test.run(); +} diff --git a/t/100-logging-t.vala b/t/100-logging-t.vala new file mode 100644 index 0000000..01c4640 --- /dev/null +++ b/t/100-logging-t.vala @@ -0,0 +1,36 @@ +// 100-logging.vala - tests of src/logging +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +void test_linit() // for coverage +{ + My.Log.linit(); + assert_true(true); +} + +void test_canonicalize() // for coverage +{ + string t; + t = My.Log.canonicalize_filename("/foo", null); + assert_true(t == "/foo"); + t = My.Log.canonicalize_filename("foo", "/bar"); + assert_true(t == "/bar/foo"); + t = My.Log.canonicalize_filename("//foo", null); + assert_true(t == "//foo"); + t = My.Log.canonicalize_filename("///foo", null); + assert_true(t == "/foo"); + t = My.Log.canonicalize_filename("/foo/./bar", null); + assert_true(t == "/foo/bar"); + t = My.Log.canonicalize_filename("/foo/bar/..", null); + assert_true(t == "/foo"); +} + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/100-logging/linit", test_linit); + Test.add_func("/100-logging/canonicalize", test_canonicalize); + + return Test.run(); +} diff --git a/t/001-basic.vala b/t/200-md4c-reader-t.vala similarity index 67% rename from t/001-basic.vala rename to t/200-md4c-reader-t.vala index ccaee57..4292f6d 100644 --- a/t/001-basic.vala +++ b/t/200-md4c-reader-t.vala @@ -1,25 +1,14 @@ -// 001-basic.vala +// t/200-md4c-reader-t.vala // Copyright (c) 2020 Christopher White. All rights reserved. // SPDX-License-Identifier: BSD-3-Clause using My; -/** - * argv[0], for use by sanity() - */ -private string program_name; -void sanity() -{ - Test.message("%s: Running sanity test in %s() at %s:%d", - program_name, GLib.Log.METHOD, GLib.Log.FILE, GLib.Log.LINE); - assert_true(true); -} - -void loadfile() +void test_loadfile() { bool did_load = false; try { - var fn = Test.build_filename(Test.FileType.DIST, "001-basic.md"); + var fn = Test.build_filename(Test.FileType.DIST, "basic.md"); Test.message("Loading filename %s", fn); var md = new MarkdownMd4cReader(); @@ -46,27 +35,20 @@ void loadfile() assert_true(node3.data.text == "Body"); } catch(FileError e) { warning("file error: %s", e.message); - assert_true(false); // make sure the test doesn't pass + assert_not_reached(); } catch(GLib.MarkupError e) { warning("%s", e.message); - assert_true(false); // make sure the test doesn't pass + assert_not_reached(); } assert_true(did_load); } public static int main (string[] args) { - program_name = args[0]; - -// // find the dir containing this executable -// var me = File.new_for_commandline_arg(args[0]); -// mydir = me.get_parent(); - // run the tests Test.init (ref args); Test.set_nonfatal_assertions(); - Test.add_func("/001-basic/sanity", sanity); - Test.add_func("/001-basic/loadfile", loadfile); + Test.add_func("/200-md4c-reader/loadfile", test_loadfile); return Test.run(); } diff --git a/t/300-pango-markup-writer-t.vala b/t/300-pango-markup-writer-t.vala new file mode 100644 index 0000000..5175451 --- /dev/null +++ b/t/300-pango-markup-writer-t.vala @@ -0,0 +1,145 @@ +// t/300-pango-markup-writer-t.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +Doc create_dummy_doc() +{ + GLib.Node root = node_of_ty(Elem.Type.ROOT); + GLib.Node node; + unowned GLib.Node unode; + + node = node_of_ty(BLOCK_COPY); + unode = node; + root.append((owned)node); + + node = node_of_ty(SPAN_PLAIN); + node.data.text = "Hello, world!"; + unode.append((owned)node); + + var retval = new Doc((owned)root); + assert_true(root == null); + assert_nonnull(retval); + return retval; +} + +void test_writefile() +{ + bool did_write = false; + File destf = null; + string destfn; + + try { + FileUtils.close(FileUtils.open_tmp("pfft-t-XXXXXX", out destfn)); + var doc = create_dummy_doc(); + + // Write it + var writer = new PangoMarkupWriter(); + writer.write_document(destfn, doc); + + // Check it + destf = File.new_for_path(destfn); + uint8[] contents; + string etag_out; + + destf.load_contents (null, out contents, out etag_out); + did_write = destf.query_exists() && (contents.length > 0); + // TODO make this check more sophisticated + + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } catch(My.Error e) { + warning("pfft error: %s", e.message); + assert_not_reached(); + } catch(GLib.Error e) { + warning("glib error: %s", e.message); + assert_not_reached(); + } + assert_true(did_write); + + // Clean up + try { + if(destf != null) { + destf.delete(); + } + } catch(GLib.Error e) { + //ignore errors + } + +} //test_writefile() + +/** Test bad inputs to write_document */ +void test_badcall() +{ + File destf = null; + string destfn; + FileUtils.close(FileUtils.open_tmp("pfft-t-XXXXXX", out destfn)); + destf = File.new_for_path(destfn); + + var writer = new PangoMarkupWriter(); + + // No filename + try { + var doc = create_dummy_doc(); + writer.write_document("", doc); // Should throw + assert_not_reached(); + } catch(My.Error e) { + printerr("got error: %s\n", e.message); + assert_true(e is My.Error.WRITER); + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } + + // Document with no nodes + try { + var node = node_of_ty(SPAN_PLAIN); + var doc = new Doc((owned)node); + doc.root = null; + writer.write_document(destfn, doc); + assert_not_reached(); + } catch(My.Error e) { + printerr("got error: %s\n", e.message); + assert_true(e is My.Error.WRITER); + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } + + // Document without a root node + try { + var node = node_of_ty(SPAN_PLAIN); + var doc = new Doc((owned)node); + writer.write_document(destfn, doc); + assert_not_reached(); + } catch(My.Error e) { + printerr("got error: %s\n", e.message); + assert_true(e is My.Error.WRITER); + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } + + // Clean up + try { + if(destf != null) { + destf.delete(); + } + } catch(GLib.Error e) { + //ignore errors + } + +} + +public static int main (string[] args) +{ + // run the tests + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/300-pango-markup-writer/writefile", test_writefile); + Test.add_func("/300-pango-markup-writer/badcall", test_badcall); + + return Test.run(); +} diff --git a/t/Makefile.am b/t/Makefile.am index 87ce2f4..497808d 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -13,7 +13,26 @@ LDADD = $(MY_use_all_ldadd) # === Programs ============================================================ -test_programs = 001-basic.t -dist_test_data = 001-basic.md +# So we don't have to list the sources of every test program individually +AM_DEFAULT_SOURCE_EXT = .vala +# But that messes up the linker selection, so: +CCLD = $(CXX) -001_basic_t_SOURCES = 001-basic.vala +test_programs = \ + 001-sanity-t \ + 010-core-util-t \ + 020-registry-t \ + 050-core-el-t \ + 060-core-template-t \ + 070-core-writer-t \ + 100-logging-t \ + 200-md4c-reader-t \ + 300-pango-markup-writer-t \ + $(EOL) + +dist_test_data = \ + 060-core-template.pfft \ + 060-no-meta.pfft \ + basic.md \ + complex.md \ + $(EOL) diff --git a/t/001-basic.md b/t/basic.md similarity index 100% rename from t/001-basic.md rename to t/basic.md From cc4f453504ebf82eab80318a78142eb3aca4daba Mon Sep 17 00:00:00 2001 From: Chris White Date: Tue, 1 Sep 2020 21:58:34 -0400 Subject: [PATCH 7/8] Added template-file support (#9) Core ==== - Add src/core/template - Add property accessors for page size, margins, header/footer text/markup - Add tests - Define `.pfft` file format. It is a GLib keyfile (INI-like) with a required `[pfft]` section. That section may someday serve as a file(1) magic string. CLI === Add --template option - Load a template from disk if -t, --template FILENAME is given - Before processing --ro, --wo, set properties from the template if the names match. --- rules.mk | 2 +- src/core/Makefile.am | 8 +- src/core/template.vala | 222 +++++++++++++++++++++ src/pfft.vala | 94 +++++++-- t/060-bad-version.pfft | 2 + t/060-core-template-t.vala | 224 ++++++++++++++++++++++ t/060-core-template.pfft | 44 +++++ t/060-empty.pfft | 2 + t/060-headfoot-markup.pfft | 19 ++ t/{060-no-meta.pfft => 060-no-magic.pfft} | 0 t/Makefile.am | 7 +- 11 files changed, 604 insertions(+), 20 deletions(-) create mode 100644 src/core/template.vala create mode 100644 t/060-bad-version.pfft create mode 100644 t/060-core-template-t.vala create mode 100644 t/060-core-template.pfft create mode 100644 t/060-empty.pfft create mode 100644 t/060-headfoot-markup.pfft rename t/{060-no-meta.pfft => 060-no-magic.pfft} (100%) diff --git a/rules.mk b/rules.mk index bbf3b0f..1f03986 100644 --- a/rules.mk +++ b/rules.mk @@ -15,7 +15,7 @@ MY_pgm_VALA = pfft.vala myconfig.vapi MY_pgm_EXTRASOURCES = pfft-shim.c # src/core -MY_core_VALA = el.vala reader.vala util.vala writer.vala registry.vala +MY_core_VALA = el.vala reader.vala registry.vala template.vala util.vala writer.vala MY_core_EXTRASOURCES = registry-impl.cpp # src/logging diff --git a/src/core/Makefile.am b/src/core/Makefile.am index 9324a33..0aceb2c 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -1,9 +1,13 @@ include $(top_srcdir)/rules.mk # All the .vala files are run through valac at once. -LOCAL_VALA_FLAGS = -H pfft-core.h --library pfft-core --vapi pfft-core.vapi +LOCAL_VALA_FLAGS = \ + -H pfft-core.h --library pfft-core --vapi pfft-core.vapi \ + --vapidir $(top_srcdir)/src/logging --pkg pfft-logging \ + $(EOL) + +LOCAL_CFLAGS = -I$(top_srcdir)/src/logging -# See also pfft-core.{vapi,h}, below EXTRA_DIST = registry.h pfft-core.h pfft-core.vapi noinst_LIBRARIES = libpfft-core.a diff --git a/src/core/template.vala b/src/core/template.vala new file mode 100644 index 0000000..4fae1f5 --- /dev/null +++ b/src/core/template.vala @@ -0,0 +1,222 @@ +// src/core/template.vala - part of pfft, https://github.com/cxw42/pfft +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My.Log; + +namespace My { + + /** + * A document template defining document appearance &c. + */ + public class Template : Object { + /** + * The data. + * + * Public for now, until I figure out a better set of accessors. + */ + public KeyFile data; + + // --- Accessors for the template's contents --- + + // Page parameters (unit suffixes: Inches, Cairo, Pango) + [Description(nick = "Paper width (in.)", blurb = "Paper width, in inches")] + public double paperwidthI { get; set; default = 8.5; } + [Description(nick = "Paper height (in.)", blurb = "Paper height, in inches")] + public double paperheightI { get; set; default = 11.0; } + [Description(nick = "Left margin (in.)", blurb = "Left margin, in inches")] + + // Margin parameters + public double lmarginI { get; set; default = 1.0; } + [Description(nick = "Top margin (in.)", blurb = "Top margin, in inches")] + public double tmarginI { get; set; default = 1.0; } + [Description(nick = "Text width (in.)", blurb = "Width of the text block, in inches")] + public double hsizeI { get; set; default = 6.5; } + [Description(nick = "Text height (in.)", blurb = "Height of the text block, in inches")] + public double vsizeI { get; set; default = 9.0; } + [Description(nick = "Footer margin (in.)", blurb = "Space between the bottom of the text block and the top of the footer, in inches")] + public double footerskipI { get; set; default = 0.3; } + [Description(nick = "Header margin (in.)", blurb = "Space between the top of the text block and the top of the header, in inches")] + public double headerskipI { get; set; default = 0.4; } + + // Header parameters + [Description(nick = "Header markup, left", blurb = "Pango markup for the header, left side")] + public string headerl { get; set; default = ""; } + [Description(nick = "Header markup, center", blurb = "Pango markup for the header, middle")] + public string headerc { get; set; default = ""; } + [Description(nick = "Header markup, right", blurb = "Pango markup for the header, right side")] + public string headerr { get; set; default = ""; } + + // Footer parameters + [Description(nick = "Footer markup, left", blurb = "Pango markup for the footer, left side")] + public string footerl { get; set; default = ""; } + [Description(nick = "Footer markup, center", blurb = "Pango markup for the footer, middle")] + public string footerc { get; set; default = "%p"; } + [Description(nick = "Footer markup, right", blurb = "Pango markup for the footer, right side")] + public string footerr { get; set; default = ""; } + + // Private properties --- leading "P" marks them to be ignored by + // other parts of pfft. + + /** Temporary holder for use with set_from_file_double(). */ + [Description(nick = "Private use")] + public double PtempI { get; set; } + + // --- Routines -------------------------------- + + /** + * Set a double parameter from the keyfile. + * + * Ignore keyfile errors; missing keys are not fatal. + */ + private void set_from_file_double(string property, string section, string key) + { + double fromfile; + try { + fromfile = data.get_double(section, key); + } catch(KeyFileError e) { + return; + } + + var wrapped = Value(typeof(double)); + wrapped.set_double(fromfile); + set_property(property, wrapped); + } + + /** + * Set a (possibly-localized) string parameter from the keyfile. + * @param property The name of the property of `this` to set + * @param section The section to look in + * @param keys The keys to try, in order. Processing stops + * as soon as one key succeeds. + * @return the index in keys that succeeded, or -1 if none succeeded. + * + * Ignore keyfile errors; missing keys are not fatal. + */ + private int set_from_file_string(string property, string section, + string[] keys) + { + int idx = -1; + int retval = -1; + + foreach(string key in keys) { + ++idx; + string fromfile; + try { + fromfile = data.get_locale_string(section, key); // default locale + } catch(KeyFileError e) { + continue; + } + + var wrapped = Value(typeof(string)); + wrapped.set_string(fromfile); + set_property(property, wrapped); + retval = idx; + break; + } + return retval; + } // set_from_file_string() + + /** Throw if the given group has both keys */ + void error_if_both_keys(string section, string k1, string k2) throws KeyFileError + { + if(data.has_key(section, k1) && data.has_key(section, k2)) { + throw new KeyFileError.PARSE(@"Section $section has keys $k1 and $k2, and I don't know which you want to use. Please remove one of them."); + } + } + + /** Default ctor --- leave all the properties at their default values. */ + public Template() + { + } + + /** Load a template file */ + public Template.from_file(string filename) + throws KeyFileError, FileError + { + data = new KeyFile(); + data.load_from_file(filename, NONE); // throws on error + + // The only required key is pfft.version. + if(!data.has_group("pfft")) { + throw new KeyFileError.GROUP_NOT_FOUND("Invalid file: 'pfft' group missing"); + } // pfft + + ldebugo(this, "Loaded key file %s", filename); + PtempI = -1; + set_from_file_double("PtempI", "pfft", "version"); + if(PtempI != 1) { // float equality OK here + throw new KeyFileError.PARSE("I don't know which file version this is"); + } + + // Load whatever data we have + if(data.has_group("page")) { + set_from_file_double("paperheightI", "page", "height"); + ldebugo(this, "paper height %f in", paperheightI); + set_from_file_double("paperwidthI", "page", "width"); + ldebugo(this, "paper width %f in", paperwidthI); + } // page + + if(data.has_group("margin")) { + set_from_file_double("headerskipI", "margin", "header"); + ldebugo(this, "header skip %f in", headerskipI); + set_from_file_double("footerskipI", "margin", "footer"); + ldebugo(this, "footer skip %f in", footerskipI); + + set_from_file_double("tmarginI", "margin", "top"); + set_from_file_double("lmarginI", "margin", "left"); + + // Margin->hsize/vsize + PtempI = -1; + set_from_file_double("PtempI", "margin", "bottom"); + if(PtempI>=0) { + vsizeI = paperheightI - tmarginI - PtempI; + } + ldebugo(this, "vsize %f in", vsizeI); + + PtempI = -1; + set_from_file_double("PtempI", "margin", "right"); + if(PtempI>=0) { + hsizeI = paperwidthI - lmarginI - PtempI; + } + ldebugo(this, "hsize %f in", hsizeI); + } // margin + + if(data.has_group("header")) { + error_if_both_keys("header", "left", "leftmarkup"); + error_if_both_keys("header", "center", "centermarkup"); + error_if_both_keys("header", "right", "rightmarkup"); + + if(1 == set_from_file_string("headerl", "header", {"leftmarkup", "left"})) { + headerl = Markup.escape_text(headerl); + } + if(1 == set_from_file_string("headerc", "header", {"centermarkup", "center"})) { + headerc = Markup.escape_text(headerc); + } + if(1 == set_from_file_string("headerr", "header", {"rightmarkup", "right"})) { + headerr = Markup.escape_text(headerr); + } + } // header + + if(data.has_group("footer")) { + error_if_both_keys("footer", "left", "leftmarkup"); + error_if_both_keys("footer", "center", "centermarkup"); + error_if_both_keys("footer", "right", "rightmarkup"); + + if(1 == set_from_file_string("footerl", "footer", {"leftmarkup", "left"})) { + footerl = Markup.escape_text(footerl); + } + if(1 == set_from_file_string("footerc", "footer", {"centermarkup", "center"})) { + footerc = Markup.escape_text(footerc); + } + if(1 == set_from_file_string("footerr", "footer", {"rightmarkup", "right"})) { + footerr = Markup.escape_text(footerr); + } + } // footer + + ldebugo(this, "Done processing template file %s", filename); + } // Template.from_file() + + } // class Template + +} diff --git a/src/pfft.vala b/src/pfft.vala index b5ee806..d96ba39 100644 --- a/src/pfft.vala +++ b/src/pfft.vala @@ -79,6 +79,9 @@ namespace My { [CCode(array_length = false)] private string[]? opt_writer_options; + /** Template filename */ + private string opt_templatefn = ""; + /** * Make command-line option descriptors * @@ -112,13 +115,16 @@ namespace My { // --wo NAME=VALUE: writer options { "wo", 0, 0, OptionArg.STRING_ARRAY, &opt_writer_options, "Set a writer option", "NAME=VALUE" }, + // --template, -t FIlENAME + { "template", 't', 0, OptionArg.FILENAME, &opt_templatefn, "Template filename", "FILENAME" }, + // FILENAME* (non-option arg(s) - inputs) { OPTION_REMAINING, 0, 0, OptionArg.FILENAME_ARRAY, &opt_infns, "Filename(s) to process", "FILENAME..." }, // list terminator { null } }; - } + } // get_options() // }}}1 // Instance data {{{1 @@ -127,6 +133,8 @@ namespace My { ClassMap writers_; string writer_default_; + Template template_ = null; + // }}}1 // Main routines {{{1 @@ -148,6 +156,8 @@ namespace My { assert_true(!readers_.is_empty); assert_true(!writers_.is_empty); + // Command-line processing + try { var opt_context = new OptionContext ("- produce a PDF from each FILENAME"); opt_context.set_help_enabled (true); @@ -163,9 +173,6 @@ namespace My { return 1; } - // Convert verbosity into GST_DEBUG levels - set_verbosity(); - if (opt_version) { print("%s\nVisit %s for more information\n", PACKAGE_STRING, PACKAGE_URL); return 0; @@ -182,7 +189,28 @@ namespace My { return 2; } - /* Create the plugins */ + // Convert verbosity into GST_DEBUG levels + set_verbosity(); + + // Load the template + if(opt_templatefn == "") { + ldebugo(this, "Using default template"); + template_ = new Template(); + } else { + try { + ldebugo(this, "Loading template from %s", opt_templatefn); + template_ = new Template.from_file(opt_templatefn); + ldebugo(this, "--- success"); + } catch(KeyFileError e) { + warning("Error processing template file %s: %s", opt_templatefn, e.message); + return 1; + } catch(FileError e) { + warning("Error loading template file %s: %s", opt_templatefn, e.message); + return 1; + } + } + + // Create the plugins Reader reader; var reader_name = opt_reader_name ?? reader_default_; try { @@ -238,7 +266,7 @@ namespace My { return 0; } // run() - void process_file(string infn, Reader reader, Writer writer) + private void process_file(string infn, Reader reader, Writer writer) throws FileError, MarkupError, RegexError, My.Error { linfo("Processing %s", infn); @@ -278,7 +306,7 @@ namespace My { } // process_file() - void set_verbosity() + private void set_verbosity() { if(opt_quiet) { Log.category.set_threshold(NONE); @@ -306,7 +334,7 @@ namespace My { // Registry functions {{{1 /** Retrieve readers and writers from the registry */ - void load_from_registry() + private void load_from_registry() { var registry = get_registry(); // print("Registry has %u keys\n", registry.size()); @@ -324,7 +352,7 @@ namespace My { } // load_from_registry() /** Retrieve information about the available readers and writers */ - string get_rw_help() + private string get_rw_help() { var sb = new StringBuilder(); if(!readers_.is_empty) { @@ -342,7 +370,7 @@ namespace My { } // get_rw_help() /** Pretty-print information from a ClassMap */ - string get_classmap_help(ClassMap m, out string default_class) + private string get_classmap_help(ClassMap m, out string default_class) { var sb = new StringBuilder(); default_class = ""; @@ -391,8 +419,12 @@ namespace My { return sb.str; } // get_classmap_help - /** Create an instance and set its properties */ - Object create_instance(ClassMap m, string class_name, + /** + * Create an instance and set its properties. + * + * Sets properties from template_ first, then from @options. + */ + private Object create_instance(ClassMap m, string class_name, string[]? options) throws KeyFileError { if(!m.has_key(class_name)) { @@ -402,13 +434,16 @@ namespace My { var type = m.get(class_name); Object retval = Object.new(type); + set_props_from_template(type, retval, template_); if(options == null) { return retval; // *** EXIT POINT *** } - // Assign the properties + // property accessor for the instance we are creating ObjectClass ocl = (ObjectClass) type.class_ref (); + + // Assign the properties var num_opts = (options == null) ? 0 : strv_length(options); for(int i=0; i%s := %s\n", retval, nv[0], nv[1]); var prop = ocl.find_property(nv[0]); - if(prop == null) { + if(prop == null || prop.get_name()[0] == 'P') { // skip unknown, private throw new KeyFileError.KEY_NOT_FOUND( "%s: %s is not an option I understand".printf( class_name, nv[0])); @@ -433,13 +468,40 @@ namespace My { class_name, nv[1], nv[0])); } - // print(" value = %s\n", Gst.Value.serialize(val)); retval.set_property(nv[0], val); + ldebugo(retval, "Set property %s from command line to %s", + nv[0], Gst.Value.serialize(val)); } // foreach option return retval; - } + } // create_instance() + private void set_props_from_template(GLib.Type instance_type, + Object instance, Template tmpl) + { + // property accessor for the instance we are creating + ObjectClass ocl = (ObjectClass) instance_type.class_ref (); + + // property accessor for the template + ObjectClass tocl = (ObjectClass) tmpl.get_type().class_ref (); + + // Set properties from the template + foreach(var tprop in tocl.list_properties()) { + string propname = tprop.get_name(); + ldebugo(instance, "Trying template property %s", propname); + var prop = ocl.find_property(propname); + if(prop == null || propname[0] == 'P' || prop.value_type != tprop.value_type) { + ldebugo(instance, "--- skipping"); + continue; + } + + Value v = Value(prop.value_type); + tmpl.get_property(propname, ref v); + instance.set_property(propname, v); + ldebugo(instance, "Set property %s from template to %s", + propname, Gst.Value.serialize(v)); + } + } // set_props_from_template() // }}}1 } // class App diff --git a/t/060-bad-version.pfft b/t/060-bad-version.pfft new file mode 100644 index 0000000..2f9f7f1 --- /dev/null +++ b/t/060-bad-version.pfft @@ -0,0 +1,2 @@ +[pfft] +version = 0 diff --git a/t/060-core-template-t.vala b/t/060-core-template-t.vala new file mode 100644 index 0000000..7fb91c5 --- /dev/null +++ b/t/060-core-template-t.vala @@ -0,0 +1,224 @@ +// 060-core-template-t.vala - tests of src/core/template.vala +// Copyright (c) 2020 Christopher White. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +using My; + +// Test loading a valid file with plaintext headers and footers +void test_load_file() +{ + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-core-template.pfft"); + var template = new Template.from_file(fn); + assert_true(template != null); + if(template == null) { + return; + } + + assert_true(template.data.has_group("pfft")); + assert_true(template.data.has_group("page")); + assert_true(template.data.has_group("margin")); + + var ver = template.data.get_integer("pfft", "version"); + assert_true(ver==1); + + // Note: direct float comparisons + assert_true(template.paperheightI == 21); + assert_true(template.paperwidthI == 22); + assert_true(template.lmarginI == 3); + assert_true(template.tmarginI == 4); + assert_true(template.vsizeI == (21-4-6)); + assert_true(template.hsizeI == (22-3-5)); + assert_true(template.headerskipI == 7); + assert_true(template.footerskipI == 8); + + assert_true(template.headerl == "Hl<"); + assert_true(template.headerc == "Hc<"); + assert_true(template.headerr == "Hr<"); + assert_true(template.footerl == "Fl<"); + assert_true(template.footerc == "Fc<"); + assert_true(template.footerr == "Fr<"); + + } catch(KeyFileError e) { + warning("keyfile error: %s", e.message); + assert_not_reached(); + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } +} + +// Test header and footer markup +void test_headfoot_markup() +{ + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-headfoot-markup.pfft"); + var template = new Template.from_file(fn); + assert_true(template != null); + if(template == null) { + return; + } + + assert_true(template.data.has_group("pfft")); + + var ver = template.data.get_integer("pfft", "version"); + assert_true(ver==1); + + // Raw markup, so the '<' doesn't get escaped + assert_true(template.headerl == "Hl<"); + assert_true(template.headerc == "Hc<"); + assert_true(template.headerr == "Hr<"); + assert_true(template.footerl == "Fl<"); + assert_true(template.footerc == "Fc<"); + assert_true(template.footerr == "Fr<"); + + } catch(KeyFileError e) { + warning("keyfile error: %s", e.message); + assert_not_reached(); + } catch(FileError e) { + warning("file error: %s", e.message); + assert_not_reached(); + } +} + +// Test header.left and header.leftmarkup in one file. I am using this as a +// proxy for the remaining five of {header,footer}.{left,center,right}*. +void test_both_headleft() +{ + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-both-headleft.pfft"); + var template = new Template.from_file(fn); + template = null; // suppress "unused" warning + assert_not_reached(); + } catch(KeyFileError e) { + diag("got keyfile error: %s", e.message); + assert_true(e is KeyFileError.PARSE); + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_not_reached(); + } +} +void test_bad_filename() +{ + File destf = null; + + try { + // make a filename that doesn't exist + string destfn; + FileUtils.close(FileUtils.open_tmp("pfft-t-XXXXXX", out destfn)); + destf = File.new_for_path(destfn); + try { + destf.delete(); + } catch(GLib.Error e) { + //ignore errors + } + + var t = new Template.from_file(destfn); + t = null; // suppress "unused" warning + assert_not_reached(); + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_true(e is FileError.FAILED || e is FileError.NOENT); + } catch { + warning("Unhandled error"); + assert_not_reached(); + } +} + +// Test an invalid file +void test_bad_file() +{ + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-no-magic.pfft"); + var template = new Template.from_file(fn); + template = null; // suppress "unused" warning + assert_not_reached(); + } catch(KeyFileError e) { + diag("got keyfile error: %s", e.message); + assert_true(e is KeyFileError.GROUP_NOT_FOUND); + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_not_reached(); + } +} + +// Test a file of a version we don't recognize +void test_bad_version() +{ + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-bad-version.pfft"); + var template = new Template.from_file(fn); + template = null; // suppress "unused" warning + assert_not_reached(); + } catch(KeyFileError e) { + diag("got keyfile error: %s", e.message); + assert_true(e is KeyFileError.PARSE); + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_not_reached(); + } +} + +// Test a valid but content-free file. Also test the default constructor, +// for coverage. +void test_empty_file() +{ + for(int which = 0; which < 2 ; ++which) { + try { + var fn = Test.build_filename(Test.FileType.DIST, "060-empty.pfft"); + Template template; + if(which == 0) { + diag(@"Loading from $fn"); + template = new Template.from_file(fn); + } else { + diag("Testing default ctor"); + template = new Template(); + } + + assert_true(template != null); + if(template == null) { + continue; + } + + // Check the default values. + // Caution: direct float comparisons + assert_true(template.paperheightI == 11); + assert_true(template.paperwidthI == 8.5); + assert_true(template.lmarginI == 1); + assert_true(template.tmarginI == 1); + assert_true(template.vsizeI == 9); + assert_true(template.hsizeI == 6.5); + assert_true(template.headerskipI == 0.4); + assert_true(template.footerskipI == 0.3); + + assert_true(template.headerl == ""); + assert_true(template.headerc == ""); + assert_true(template.headerr == ""); + assert_true(template.footerl == ""); + assert_true(template.footerc == "%p"); + assert_true(template.footerr == ""); + + } catch(KeyFileError e) { + diag("got keyfile error: %s", e.message); + assert_not_reached(); + } catch(FileError e) { + diag("got file error: %s", e.message); + assert_not_reached(); + } + } +} + +public static int main (string[] args) +{ + Test.init (ref args); + Test.set_nonfatal_assertions(); + Test.add_func("/060-core-template/load_file", test_load_file); + Test.add_func("/060-core-template/headfoot_markup", test_headfoot_markup); + Test.add_func("/060-core-template/both_headleft", test_both_headleft); + Test.add_func("/060-core-template/bad_filename", test_bad_filename); + Test.add_func("/060-core-template/bad_file", test_bad_file); + Test.add_func("/060-core-template/bad_version", test_bad_version); + Test.add_func("/060-core-template/empty_file", test_empty_file); + + return Test.run(); +} diff --git a/t/060-core-template.pfft b/t/060-core-template.pfft new file mode 100644 index 0000000..b92e455 --- /dev/null +++ b/t/060-core-template.pfft @@ -0,0 +1,44 @@ +# t/060-core-template.pfft +# A test with deliberately odd, but valid, values + +[pfft] +version = 1 + +[page] +# TODO support units. For now, everything is in inches. +# Maybe https://github.com/orangeduck/mpc ? + +# paperheightI - Paper height, in inches +height = 21 +# paperwidthI - Paper width, in inches +width = 22 + +[margin] +# lmarginI - Left margin, in inches +left = 3 +# tmarginI - Top margin, in inches +top = 4 + +# Do these as margins instead +# hsizeI - Width of the text block, in inches +# vsizeI - Height of the text block, in inches +right = 5 +bottom = 6 + +# footerskipI - Space between the bottom of the text block and the top of the footer, in inches +# headerskipI - Space between the top of the text block and the top of the header, in inches +header = 7 +footer = 8 + +# Header and footer: test for escaping of markup +[header] +left = Hl< +center = Hc< +right = Hr< + +[footer] +left = Fl< +center = Fc< +right = Fr< + +# vi: set ft=desktop: # diff --git a/t/060-empty.pfft b/t/060-empty.pfft new file mode 100644 index 0000000..e673328 --- /dev/null +++ b/t/060-empty.pfft @@ -0,0 +1,2 @@ +[pfft] +version = 1 diff --git a/t/060-headfoot-markup.pfft b/t/060-headfoot-markup.pfft new file mode 100644 index 0000000..ab39f69 --- /dev/null +++ b/t/060-headfoot-markup.pfft @@ -0,0 +1,19 @@ +# t/060-headfoot-markup.pfft +# A test with header and footer markup + +[pfft] +version = 1 + +# Header and footer: test for markup passed through. These don't have to be +# valid markup strings; we're just checking that they don't get escaped. +[header] +leftmarkup = Hl< +centermarkup = Hc< +rightmarkup = Hr< + +[footer] +leftmarkup = Fl< +centermarkup = Fc< +rightmarkup = Fr< + +# vi: set ft=desktop: # diff --git a/t/060-no-meta.pfft b/t/060-no-magic.pfft similarity index 100% rename from t/060-no-meta.pfft rename to t/060-no-magic.pfft diff --git a/t/Makefile.am b/t/Makefile.am index 497808d..9a19d6a 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -31,8 +31,13 @@ test_programs = \ $(EOL) dist_test_data = \ + 060-bad-version.pfft \ + 060-both-headleft.pfft \ 060-core-template.pfft \ - 060-no-meta.pfft \ + 060-empty.pfft \ + 060-headfoot-markup.pfft \ + 060-no-magic.pfft \ basic.md \ complex.md \ $(EOL) +: From 58382479787824a99080670b62de1638ce9b76fb Mon Sep 17 00:00:00 2001 From: Chris White Date: Mon, 7 Sep 2020 18:04:36 -0400 Subject: [PATCH 8/8] pango-markup: Add custom headers/footers - add header/footer properties - render headers, footers in smaller text than the main copy - support `%%` and `%p` replacements. --- README.md | 7 ++ src/core/writer.vala | 4 +- src/pfft.vala | 4 +- src/reader/md4c-reader.vala | 2 +- src/writer/dumper.vala | 3 +- src/writer/pango-blocks.vala | 19 +++-- src/writer/pango-markup.vala | 156 +++++++++++++++++++++++++++++------ 7 files changed, 156 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 2832a52..d9733fa 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,13 @@ Before submitting a PR, please run `make prep`. This will: be left in the tree. To remove the generated C files, `make maintainer-clean`. +### Design decisions + +- Decisions about the exact appearance of an item should be made as late + as possible. For example, in the `pango-markup` writer (the default), + headers and footers are set in smaller type by the writer, not the upstream + code that feeds markup to the writer. + ## Thanks - diff --git a/src/core/writer.vala b/src/core/writer.vala index 45aeb93..57e2d73 100644 --- a/src/core/writer.vala +++ b/src/core/writer.vala @@ -17,8 +17,10 @@ namespace My { * @param sourcefn The filename of the source that @doc came from. * This can be used, e.g., to resolve relative paths * to images. + * @param template The template selected by the user, if any */ - public abstract void write_document(string filename, Doc doc, string? sourcefn = null) + public abstract void write_document(string filename, Doc doc, + string? sourcefn = null) throws FileError, My.Error; /** diff --git a/src/pfft.vala b/src/pfft.vala index d96ba39..d2e2813 100644 --- a/src/pfft.vala +++ b/src/pfft.vala @@ -470,7 +470,9 @@ namespace My { retval.set_property(nv[0], val); ldebugo(retval, "Set property %s from command line to %s", - nv[0], Gst.Value.serialize(val)); + nv[0], val.type() == typeof(string) ? @"'$(val.get_string())'" : + Gst.Value.serialize(val)); + } // foreach option return retval; diff --git a/src/reader/md4c-reader.vala b/src/reader/md4c-reader.vala index cfdd572..a94cfad 100644 --- a/src/reader/md4c-reader.vala +++ b/src/reader/md4c-reader.vala @@ -177,7 +177,7 @@ namespace My break; case CODE: newnode = node_of_ty(SPAN_CODE); break; case DEL: newnode = node_of_ty(SPAN_STRIKE); break; - case U: newnode = node_of_ty(SPAN_UNDERLINE); break; + case SpanType.U: newnode = node_of_ty(SPAN_UNDERLINE); break; default: printerr("Unsupported span type %s\n".printf(span_type.to_string())); newnode = node_of_ty(SPAN_PLAIN); diff --git a/src/writer/dumper.vala b/src/writer/dumper.vala index 49fe157..23ff614 100644 --- a/src/writer/dumper.vala +++ b/src/writer/dumper.vala @@ -9,7 +9,8 @@ namespace My { [Description(blurb = "Dump pfft's internal representation of the document (for debugging)")] public bool meta { get; default = false; } - public void write_document(string filename, Doc doc, string? source_fn = null) + public void write_document(string filename, Doc doc, + string? source_fn = null) throws FileError, My.Error { emit(filename, doc.as_string()); diff --git a/src/writer/pango-blocks.vala b/src/writer/pango-blocks.vala index 9fae990..1d51348 100644 --- a/src/writer/pango-blocks.vala +++ b/src/writer/pango-blocks.vala @@ -56,12 +56,13 @@ namespace My { /** * Render this shape at the current position. - * @param cr The Cairo context - * @param do_path See CairoShapeRendererFunc * * Leaves the current position at the right side of the rendering. * This is because shapes are inline (span-like), so there is * more text following the shape, in the general case. + * + * @param cr The Cairo context + * @param do_path See CairoShapeRendererFunc */ public abstract void render(Cairo.Context cr, bool do_path); @@ -71,10 +72,10 @@ namespace My { } // class Base /** - * An image + * An image. * * This class is based on C code by Mike Birch, which code he kindly - * placed in the public domain. . + * placed in the public domain. [[https://immortalsofar.com/PangoDemo/]]. */ public class Image : Base { /** @@ -208,13 +209,14 @@ namespace My { /** * Create an Image referencing an external image file. + * + * NOTE: at present, assumes that @href is a path from the + * location of the source file to the location of a PNG file. + * * @param href Where the referenced image is * @param doc_path Where the referencing file is. * This is a string rather than a File so it * can later be expanded to URLs. - * - * NOTE: at present, assumes that @href is a path from the - * location of the source file to the location of a PNG file. */ public Image.from_href(string href, string doc_path, int paddingP = -1) { @@ -486,10 +488,11 @@ namespace My { /** * Initialize shape_attrs. - * @param text The actual text in the layout --- NOT the markup * * Call only when the markup for the block has been finalized and * all add_shape() calls have been made. + * + * @param text The actual text in the layout --- NOT the markup */ protected void fill_shape_attrs(string text) { diff --git a/src/writer/pango-markup.vala b/src/writer/pango-markup.vala index c58c208..e711524 100644 --- a/src/writer/pango-markup.vala +++ b/src/writer/pango-markup.vala @@ -84,7 +84,7 @@ namespace My { /** The Pango layout for bullets and numbers */ Pango.Layout bullet_layout = null; - /** The Pango layout for the page numbers and headers */ + /** The Pango layout for the */ Pango.Layout pageno_layout = null; /** Current page */ @@ -108,6 +108,22 @@ namespace My { [Description(nick = "Header margin (in.)", blurb = "Space between the top of the text block and the top of the header, in inches")] public double headerskipI { get; set; default = 0.4; } + // Header parameters + [Description(nick = "Header markup, left", blurb = "Pango markup for the header, left side")] + public string headerl { get; set; default = ""; } + [Description(nick = "Header markup, center", blurb = "Pango markup for the header, middle")] + public string headerc { get; set; default = ""; } + [Description(nick = "Header markup, right", blurb = "Pango markup for the header, right side")] + public string headerr { get; set; default = ""; } + + // Footer parameters + [Description(nick = "Footer markup, left", blurb = "Pango markup for the footer, left side")] + public string footerl { get; set; default = ""; } + [Description(nick = "Footer markup, center", blurb = "Pango markup for the footer, middle")] + public string footerc { get; set; default = "%p"; } + [Description(nick = "Footer markup, right", blurb = "Pango markup for the footer, right side")] + public string footerr { get; set; default = ""; } + /** Used in process_node_into() */ private Regex re_newline = null; @@ -120,11 +136,13 @@ namespace My { private Regex re_command = null; /** - * Header markup + * Regex for placeholders in headers/footers. * - * TODO handle headers/footers in a more general way + * Currently supported placeholders are: + * * `%p`: page number + * * `%%`: a literal percent sign */ - private string header_markup = ""; + private Regex re_hf_placeholder = null; // Not a ctor since we create instances through g_object_new() --- see // https://gitlab.gnome.org/GNOME/vala/-/issues/650 @@ -132,6 +150,9 @@ namespace My { try { re_newline = new Regex("\\R+"); re_command = new Regex("^pfft:\\s*(\\w+)"); + re_hf_placeholder = new Regex("%(?[p%])(?!\\w)"); + // can't use \b in place of the negative lookahead because + // \b doesn't match between two non-word chars } catch(RegexError e) { // LCOV_EXCL_START lerroro(this, "Could not create required regexes --- I can't go on"); assert(false); // die horribly --- something is very wrong! @@ -145,11 +166,11 @@ namespace My { * @param sourcefn The filename of the source that @doc came from. * TODO make paper size a parameter */ - public void write_document(string filename, Doc doc, string? sourcefn = null) throws FileError, My.Error + public void write_document(string filename, Doc doc, string? sourcefn = null) + throws FileError, My.Error { source_fn = (sourcefn == null) ? "" : sourcefn; - int hsizeP = i2p(hsizeI); int rightP = i2p(lmarginI+hsizeI); int bottomP = i2p(tmarginI+vsizeI); @@ -166,8 +187,6 @@ namespace My { bullet_layout = Blocks.new_layout_12pt(cr); pageno_layout = Blocks.new_layout_12pt(cr); // Layout for page numbers - pageno_layout.set_width(hsizeP); - pageno_layout.set_alignment(CENTER); cr.move_to(i2c(lmarginI), i2c(tmarginI)); // over, down (respectively) from the UL corner @@ -221,20 +240,10 @@ namespace My { void eject_page() { linfoo(this, "Finalizing page %d", pageno); - - // render the page header on the page we just finished - if(header_markup != "") { - pageno_layout.set_markup(header_markup, -1); - cr.move_to(i2c(lmarginI), i2c(tmarginI-headerskipI)); - Pango.cairo_show_layout(cr, pageno_layout); - } - - // render the page number on the page we just finished - pageno_layout.set_text(pageno.to_string(), -1); - cr.move_to(i2c(lmarginI), i2c(tmarginI+vsizeI+footerskipI)); - Pango.cairo_show_layout(cr, pageno_layout); + render_headers_footers(); cr.show_page(); + // Start the next page ++pageno; cr.new_path(); cr.move_to(i2c(lmarginI), i2c(tmarginI)); @@ -246,6 +255,106 @@ namespace My { } // eject_page() + /** render the page header(s)/footer(s) on the page we just finished */ + private void render_headers_footers() + { + int hsizeP = i2p(hsizeI); + + double headeryI = tmarginI-headerskipI; + render_one_hf("headerL", headerl, hsizeP, LEFT, lmarginI, headeryI); + render_one_hf("headerC", headerc, hsizeP, CENTER, lmarginI, headeryI); + render_one_hf("headerR", headerr, hsizeP, RIGHT, lmarginI, headeryI); + + double footeryI = tmarginI+vsizeI+footerskipI; + render_one_hf("footerL", footerl, hsizeP, LEFT, lmarginI, footeryI); + render_one_hf("footerC", footerc, hsizeP, CENTER, lmarginI, footeryI); + render_one_hf("footerR", footerr, hsizeP, RIGHT, lmarginI, footeryI); + } + + /** Replace "%p" and other placeholders in header/footer text */ + private bool replace_hf_placeholders (string ident, MatchInfo match_info, StringBuilder result) + { + bool ok = false; + ltraceo(this, "HF %s: checking placeholders", ident); + + do { // once + if(match_info.get_match_count() == 0) { + break; + } + + string which = match_info.fetch_named("which"); + llogo(this, "placeholder %s", which != null ? which : ""); + if(which == null) { + break; + } + + switch(which) { + case "p": + result.append(pageno.to_string()); + ok = true; + break; + case "%": + result.append("%"); + ok = true; + break; + default: + break; + } + } while(false); + + if(!ok) { + string fullmatch = match_info.fetch(0); + if(fullmatch == null) { + fullmatch = ""; + } + lwarningo(this, @"I don't understand the placeholder '$fullmatch'"); + } + + return false; // keep going + } + + /** + * Render one header or footer. + * + * @param ident Which header/footer. Only used for log messages. + * @param markup The Pango markup to render + * @param widthP The width to use for the layout + * @param align The alignment to use for the layout + * @param leftI Where to render (X) with respect to the page + * @param topI Where to render (Y) with respect to the page + */ + private void render_one_hf(string ident, string markup, int widthP, + Pango.Alignment align, double leftI, + double topI) + { + if(markup == "") { + ltraceo(this, "HF %s: Skipping --- no markup", ident); + return; + } + ltraceo(this, "HF %s: Processing markup -%s-", ident, markup); + + string m2; // modified markup post placeholder processing + try { + m2 = re_hf_placeholder.replace_eval(markup, -1, 0, 0, + (m, s)=>{ return replace_hf_placeholders(ident, m, s); }); + } catch(RegexError e) { + lwarningo(this, "Got regex error: %s", e.message); + m2 = markup; + } + + // By default, make the text smaller. The user can override this + // with an express ``. + m2 = @"$m2"; + + ltraceo(this, "HF %s: Rendering", ident); + pageno_layout.set_width(widthP); + pageno_layout.set_alignment(align); + pageno_layout.set_markup(m2, -1); + cr.move_to(i2c(leftI), i2c(topI)); + Pango.cairo_show_layout(cr, pageno_layout); + ltraceo(this, "HF %s: Done", ident); + } + /////////////////////////////////////////////////////////////////// // Generate blocks of markup from a Doc. // The methods in this section assume cr and layout members are valid @@ -560,13 +669,6 @@ namespace My { case "": // not a command break; - case "header": - header_markup = blk.markup; - header_markup._strip(); - linfoo(this, "Header markup set to -%s-", header_markup); - blk = null; // discard the Blk we used to collect the text - blk = new Blk(layout); - break; default: lwarningo(this, "Ignoring unknown command '%s'", cmd); break;