From 8f9a80a9ac8f353ce2cdafa23f0e5163d30d5800 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 1 May 2019 22:32:11 +0300 Subject: Add support for description-type package manifest value --- mod/buildfile | 4 +- mod/mod-package-details.cxx | 16 +++- mod/mod-package-version-details.cxx | 10 +- mod/mod-repository-root.cxx | 6 ++ mod/page.cxx | 179 +++++++++++++++++++++++++++++++++--- mod/page.hxx | 124 ++++++++++++++++++------- 6 files changed, 289 insertions(+), 50 deletions(-) (limited to 'mod') diff --git a/mod/buildfile b/mod/buildfile index bd68bd0..ffa9031 100644 --- a/mod/buildfile +++ b/mod/buildfile @@ -11,7 +11,9 @@ mod{*}: install = libexec/ } -import libs = libodb%lib{odb} +import libs = libcmark-gfm%lib{cmark-gfm} +import libs += libcmark-gfm-extensions%lib{cmark-gfm-extensions} +import libs += libodb%lib{odb} import libs += libodb-pgsql%lib{odb-pgsql} import libs += libbutl%lib{butl} import libs += libbpkg%lib{bpkg} diff --git a/mod/mod-package-details.cxx b/mod/mod-package-details.cxx index 28370c2..fa073b5 100644 --- a/mod/mod-package-details.cxx +++ b/mod/mod-package-details.cxx @@ -186,13 +186,17 @@ handle (request& rq, response& rs) if (const optional& d = pkg->description) { const string id ("description"); + const string what (name.string () + " description"); s << (full - ? PRE_TEXT (*d, id) - : PRE_TEXT (*d, + ? DIV_TEXT (*d, *pkg->description_type, id, what, error) + : DIV_TEXT (*d, + *pkg->description_type, options_->package_description (), url (!full, squery, page, id), - id)); + id, + what, + error)); } s << TABLE(CLASS="proplist", ID="package") @@ -221,7 +225,11 @@ handle (request& rq, response& rs) package_db_->query_value ( search_params (squery, tenant, name))); - s << FORM_SEARCH (squery, "q") + // Let's disable autofocus in the full page mode since clicking the full or + // more link the user most likely intends to read rather than search, while + // autofocus scrolls the page to the search field. + // + s << FORM_SEARCH (squery, "q", !full) << DIV_COUNTER (pkg_count, "Version", "Versions"); // Enclose the subsequent tables to be able to use nth-child CSS selector. diff --git a/mod/mod-package-version-details.cxx b/mod/mod-package-version-details.cxx index 4e09c80..82221b4 100644 --- a/mod/mod-package-version-details.cxx +++ b/mod/mod-package-version-details.cxx @@ -184,13 +184,17 @@ handle (request& rq, response& rs) if (const optional& d = pkg->description) { const string id ("description"); + const string what (title + " description"); s << (full - ? PRE_TEXT (*d, id) - : PRE_TEXT (*d, + ? DIV_TEXT (*d, *pkg->description_type, id, what, error) + : DIV_TEXT (*d, + *pkg->description_type, options_->package_description (), url (!full, id), - id)); + id, + what, + error)); } const repository_location& rl (pkg->internal_repository.load ()->location); diff --git a/mod/mod-repository-root.cxx b/mod/mod-repository-root.cxx index fc13aca..ed170c9 100644 --- a/mod/mod-repository-root.cxx +++ b/mod/mod-repository-root.cxx @@ -6,6 +6,8 @@ #include // tzset() +#include + #include #include // find() @@ -285,6 +287,10 @@ namespace brep // sub-handlers, while handling requests). // tzset (); + + // To recognize cmark-gfm extensions while parsing Markdown later on. + // + cmark_gfm_core_extensions_ensure_registered (); } bool repository_root:: diff --git a/mod/page.cxx b/mod/page.cxx index f780b95..63bb495 100644 --- a/mod/page.cxx +++ b/mod/page.cxx @@ -4,6 +4,9 @@ #include +#include +#include + #include #include // hex, uppercase, right #include @@ -13,6 +16,7 @@ #include #include +#include #include #include @@ -98,8 +102,12 @@ namespace brep << TBODY << TR << TD(ID="search-txt") - << *INPUT(TYPE="search", NAME=name_, VALUE=query_, - AUTOFOCUS="") + << INPUT(TYPE="search", NAME=name_, VALUE=query_); + + if (autofocus_) + s << AUTOFOCUS(""); + + s << ~INPUT << ~TD << TD(ID="search-btn") << *INPUT(TYPE="submit", VALUE="Search") @@ -805,34 +813,183 @@ namespace brep // PRE_TEXT // - void PRE_TEXT:: - operator() (serializer& s) const + static void + serialize_pre_text (serializer& s, + const string& text, + size_t length, + const string* url, + const string& id) { - if (text_.empty ()) + if (text.empty ()) return; - size_t n (text_.find_first_of (" \t\n", length_)); + size_t n (text.find_first_of (" \t\n", length)); bool full (n == string::npos); // Text length is below the limit. // Truncate the text if length exceeds the limit. // - const string& t (full ? text_ : string (text_, 0, n)); + const string& t (full ? text : string (text, 0, n)); s << PRE; - if (!id_.empty ()) - s << ID(id_); + if (!id.empty ()) + s << ID(id); s << t; if (!full) { - assert (url_ != nullptr); - s << "... " << A(HREF=*url_) << "More" << ~A; + assert (url != nullptr); + s << "... " << A(HREF=*url) << "More" << ~A; } s << ~PRE; } + void PRE_TEXT:: + operator() (serializer& s) const + { + serialize_pre_text (s, text_, length_, url_, id_); + } + + // DIV_TEXT + // + void DIV_TEXT:: + operator() (serializer& s) const + { + switch (type_) + { + case text_type::plain: + { + // To keep things regular we wrap the preformatted text into
. + // + s << DIV(ID=id_, CLASS="plain"); + serialize_pre_text (s, text_, length_, url_, "" /* id */); + s << ~DIV; + break; + } + case text_type::common_mark: + case text_type::github_mark: + { + // Convert Markdown into XHTML wrapping it into the
element. + // + auto print_error = [&s, this] (const string& e) + { + s << DIV(ID=id_, CLASS="markdown") + << SPAN(CLASS="error") << e << ~SPAN + << ~DIV; + }; + + // Note that the only possible reason for the following cmark API + // calls to fail is the inability to allocate memory. Unfortunately, + // instead of reporting the failure to the caller, the API issues + // diagnostics to stderr and aborts the process. Let's decrease the + // probability of such an event by limiting the text size to 64K. + // + if (text_.size () > 64 * 1024) + { + print_error (what_ + " is too long"); + return; + } + + string html; + { + char* r; + { + // Parse Markdown into the AST. + // + unique_ptr parser ( + cmark_parser_new (CMARK_OPT_DEFAULT | CMARK_OPT_VALIDATE_UTF8), + [] (cmark_parser* p) {cmark_parser_free (p);}); + + // Enable GitHub extensions in the parser, if requested. + // + if (type_ == text_type::github_mark) + { + auto add = [&parser] (const char* ext) + { + cmark_syntax_extension* e ( + cmark_find_syntax_extension (ext)); + + // Built-in extension is only expected. + // + assert (e != nullptr); + + cmark_parser_attach_syntax_extension (parser.get (), e); + }; + + add ("table"); + add ("strikethrough"); + add ("autolink"); + + // Somehow feels unsafe (there are some nasty warnings when + // upstream's tasklist.c is compiled), so let's disable for now. + // + // add ("tasklist"); + } + + cmark_parser_feed (parser.get (), text_.c_str (), text_.size ()); + + unique_ptr doc ( + cmark_parser_finish (parser.get ()), + [] (cmark_node* n) {cmark_node_free (n);}); + + // Render the AST into an XHTML fragment. + // + // Note that unlike GitHub we follow the default API behavior and + // don't allow the raw HTML in Markdown (omitting the + // CMARK_OPT_UNSAFE flag). This way we can assume the rendered + // HTML is a well-formed XHTML fragment, which we rely upon for + // truncation (see below). Note that by default the renderer + // suppresses any HTML-alike markup and unsafe URLs (javascript:, + // etc). + // + r = cmark_render_html (doc.get (), + CMARK_OPT_DEFAULT, + nullptr /* extensions */); + } + + unique_ptr deleter ( + r, + [] (char* s) {cmark_get_default_mem_allocator ()->free (s);}); + + html = r; + } + + // From the CommonMark Spec it follows that the resulting HTML can be + // assumed a well-formed XHTML fragment with all the elements having + // closing tags. But let's not assume this being the case (due to some + // library bug or similar) and handle the xml::parsing exception. + // + try + { + fragment f (html, "gfm-html", url_ == nullptr ? 0 : length_); + + s << DIV(ID=id_, CLASS="markdown"); + + // Disable indentation not to introduce unwanted spaces. + // + s.suspend_indentation (); + s << f; + s.resume_indentation (); + + if (f.truncated) + s << DIV(CLASS="more") << A(HREF=*url_) << "More" << ~A << ~DIV; + + s << ~DIV; + } + catch (const xml::parsing& e) + { + string error ("unable to parse " + what_ + " XHTML fragment: " + + e.what ()); + diag_ << error; + print_error (error); + } + + break; + } + } + } + // DIV_PAGER // DIV_PAGER:: diff --git a/mod/page.hxx b/mod/page.hxx index b5c62c1..1138a80 100644 --- a/mod/page.hxx +++ b/mod/page.hxx @@ -17,6 +17,7 @@ #include #include +#include #include // page_menu namespace brep @@ -24,7 +25,7 @@ namespace brep // Page common building blocks. // - // Generates CSS link elements. + // Generate CSS link elements. // class CSS_LINKS { @@ -39,7 +40,7 @@ namespace brep const dir_path& root_; }; - // Generates page header element. + // Generate page header element. // class DIV_HEADER { @@ -60,13 +61,16 @@ namespace brep const string& tenant_; }; - // Generates package search form element with the specified query input + // Generate package search form element with the specified query input // element name. // class FORM_SEARCH { public: - FORM_SEARCH (const string& q, const string& n): query_ (q), name_ (n) {} + FORM_SEARCH (const string& q, const string& n, bool a = true) + : query_ (q), name_ (n), autofocus_ (a) + { + } void operator() (xml::serializer&) const; @@ -74,9 +78,10 @@ namespace brep private: const string& query_; const string& name_; + bool autofocus_; }; - // Generates counter element. + // Generate counter element. // // It could be redunant to distinguish between singular and plural word forms // if it wouldn't be so cheap in English, and phrase '1 Packages' wouldn't @@ -97,7 +102,7 @@ namespace brep const char* plural_; }; - // Generates table row element, that has the 'label: value' layout. + // Generate table row element, that has the 'label: value' layout. // class TR_VALUE { @@ -113,7 +118,7 @@ namespace brep const string& value_; }; - // Generates table row element, that has the 'label: ' + // Generate table row element, that has the 'label: ' // layout. // class TR_INPUT @@ -139,7 +144,7 @@ namespace brep bool autofocus_; }; - // Generates table row element, that has the 'label: ' + // Generate table row element, that has the 'label: ' // layout. Option elements are represented as a list of value/inner-text // pairs. // @@ -162,7 +167,7 @@ namespace brep const vector>& options_; }; - // Generates tenant id element. + // Generate tenant id element. // // Displays a link to the service page for the specified tenant. // @@ -185,7 +190,7 @@ namespace brep const string& tenant_; }; - // Generates package name element with an optional search criteria. The + // Generate package name element with an optional search criteria. The // search string should be url-encoded, if specified. // class TR_NAME @@ -207,7 +212,7 @@ namespace brep const string& tenant_; }; - // Generates package version element. + // Generate package version element. // class TR_VERSION { @@ -248,7 +253,7 @@ namespace brep const string* tenant_; }; - // Generates package project name element. + // Generate package project name element. // // Displays a link to the package search page with the project name // specified as a keyword. @@ -268,7 +273,7 @@ namespace brep const string& tenant_; }; - // Generates package summary element. + // Generate package summary element. // class TR_SUMMARY { @@ -282,7 +287,7 @@ namespace brep const string& summary_; }; - // Generates package license alternatives element. + // Generate package license alternatives element. // class TR_LICENSE { @@ -296,7 +301,7 @@ namespace brep const license_alternatives& licenses_; }; - // Generates package license alternatives elements. Differs from TR_LICENSE + // Generate package license alternatives elements. Differs from TR_LICENSE // by producing multiple rows instead of a single one. // class TR_LICENSES @@ -311,7 +316,7 @@ namespace brep const license_alternatives& licenses_; }; - // Generates package tags element. + // Generate package tags element. // class TR_TAGS { @@ -340,7 +345,7 @@ namespace brep const string& tenant_; }; - // Generates package dependencies element. + // Generate package dependencies element. // class TR_DEPENDS { @@ -357,7 +362,7 @@ namespace brep const string& tenant_; }; - // Generates package requirements element. + // Generate package requirements element. // class TR_REQUIRES { @@ -371,7 +376,7 @@ namespace brep const requirements& requirements_; }; - // Generates url element. + // Generate url element. // class TR_URL { @@ -386,7 +391,7 @@ namespace brep const char* label_; }; - // Generates email element. + // Generate email element. // class TR_EMAIL { @@ -402,7 +407,7 @@ namespace brep const char* label_; }; - // Generates package version priority element. + // Generate package version priority element. // class TR_PRIORITY { @@ -416,7 +421,7 @@ namespace brep const priority& priority_; }; - // Generates repository name element. + // Generate repository name element. // class TR_REPOSITORY { @@ -433,7 +438,7 @@ namespace brep const string& tenant_; }; - // Generates repository location element. + // Generate repository location element. // class TR_LOCATION { @@ -447,7 +452,7 @@ namespace brep const repository_location& location_; }; - // Generates package download URL element. + // Generate package download URL element. // class TR_DOWNLOAD { @@ -461,7 +466,7 @@ namespace brep const string& url_; }; - // Generates sha256sum element. + // Generate sha256sum element. // class TR_SHA256SUM { @@ -475,7 +480,7 @@ namespace brep const string& sha256sum_; }; - // Generates build results element. + // Generate build results element. // class TR_BUILD_RESULT { @@ -492,7 +497,7 @@ namespace brep const dir_path& root_; }; - // Generates comment element. + // Generate comment element. // class SPAN_COMMENT { @@ -506,7 +511,7 @@ namespace brep const string& comment_; }; - // Generates package build result status element. + // Generate package build result status element. // class SPAN_BUILD_RESULT_STATUS { @@ -520,7 +525,7 @@ namespace brep const bbot::result_status& status_; }; - // Generates paragraph elements converting a plain text into XHTML5 applying + // Generate paragraph elements converting a plain text into XHTML5 applying // some heuristics (see implementation for details). Truncate the text if // requested. // @@ -551,7 +556,7 @@ namespace brep string id_; }; - // Generates pre-formatted text element. Truncate the text if requested. + // Generate pre-formatted text element. Truncate the text if requested. // class PRE_TEXT { @@ -579,7 +584,64 @@ namespace brep string id_; }; - // Generates paging element. + // Generate a typed text element truncating it if requested. On the + // underlying parsing/rendering error, log it and generate the error + // description element instead. Note that such an error indicates an issue + // with the implementation, rather than with the specified text. + // + class DIV_TEXT + { + public: + // Generate a full text element. + // + DIV_TEXT (const string& t, + text_type tp, + const string& id, + const string& what, + const basic_mark& diag) + : text_ (t), + type_ (tp), + length_ (t.size ()), + url_ (nullptr), + id_ (id), + what_ (what), + diag_ (diag) + { + } + + // Generate a brief text element. + // + DIV_TEXT (const string& t, + text_type tp, + size_t l, + const string& u, + const string& id, + const string& what, + const basic_mark& diag) + : text_ (t), + type_ (tp), + length_ (l), + url_ (&u), + id_ (id), + what_ (what), + diag_ (diag) + { + } + + void + operator() (xml::serializer&) const; + + private: + const string& text_; + text_type type_; + size_t length_; + const string* url_; // Full page url. + string id_; + const string& what_; + const basic_mark& diag_; + }; + + // Generate paging element. // class DIV_PAGER { -- cgit v1.1