From 1ce84922e3008cad6cf1b9056b705f2642bd3772 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 28 Oct 2015 17:56:30 +0200 Subject: WEB pages re-styling --- brep/buildfile | 2 +- brep/module.cxx | 28 +- brep/options.cli | 20 +- brep/package-details | 32 +++ brep/package-details.cxx | 225 +++++++++++++++ brep/package-search.cxx | 86 +++--- brep/package-version-details.cxx | 267 ++++++++--------- brep/package-version-search | 32 --- brep/package-version-search.cxx | 179 ------------ brep/page | 287 ++++++++++++++++--- brep/page.cxx | 604 +++++++++++++++++++++++++++++++-------- brep/services.cxx | 10 +- 12 files changed, 1185 insertions(+), 587 deletions(-) create mode 100644 brep/package-details create mode 100644 brep/package-details.cxx delete mode 100644 brep/package-version-search delete mode 100644 brep/package-version-search.cxx (limited to 'brep') diff --git a/brep/buildfile b/brep/buildfile index 79dadc2..808de15 100644 --- a/brep/buildfile +++ b/brep/buildfile @@ -21,7 +21,7 @@ libso{brep}: cxx.export.poptions = -I$out_root -I$src_root # import libs += libstudxml%lib{studxml} -brep = cxx{diagnostics module services package-search package-version-search \ +brep = cxx{diagnostics module services package-search package-details \ package-version-details shared-database page} cli.cxx{options} web = ../web/apache/cxx{request service} ../web/cxx{mime-url-encoding} diff --git a/brep/module.cxx b/brep/module.cxx index 62c70c9..fa7f479 100644 --- a/brep/module.cxx +++ b/brep/module.cxx @@ -89,7 +89,9 @@ namespace brep for (const auto& nv: options) { argv.push_back (nv.name.c_str ()); - argv.push_back (nv.value.c_str ()); + + if (nv.value) + argv.push_back (nv.value->c_str ()); } int argc (argv.size ()); @@ -252,7 +254,7 @@ namespace brep peek () { if (i_ != name_values_.end ()) - return name_ ? i_->name.c_str () : i_->value.c_str (); + return name_ ? i_->name.c_str () : i_->value->c_str (); else throw eos_reached (); } @@ -262,12 +264,8 @@ namespace brep { if (i_ != name_values_.end ()) { - const char* r (name_ ? i_->name.c_str () : i_->value.c_str ()); - - if (!name_) - ++i_; - - name_ = !name_; + const char* r (name_ ? i_->name.c_str () : i_->value->c_str ()); + skip (); return r; } else @@ -279,10 +277,18 @@ namespace brep { if (i_ != name_values_.end ()) { - if (!name_) + if (name_) + { + if (i_->value) + name_ = false; + else + ++i_; + } + else + { ++i_; - - name_ = !name_; + name_ = true; + } } else throw eos_reached (); diff --git a/brep/options.cli b/brep/options.cli index 90063c1..86cb70c 100644 --- a/brep/options.cli +++ b/brep/options.cli @@ -28,14 +28,17 @@ namespace brep std::uint16_t pages-in-pager = 10; }; - class package_version_search: module, db + class package_details: module, db { std::uint16_t results-on-page = 10; std::uint16_t pages-in-pager = 10; + std::uint16_t description-length = 400; }; class package_version_details: module, db { + std::uint16_t description-length = 400; + std::uint16_t changes-length = 800; }; } @@ -43,7 +46,8 @@ namespace brep // namespace params { - // Use parameters long names in the C++ code, short aliases in HTTP URL. + // Use parameters long names in the C++ code, short aliases (if present) + // in HTTP URL. // class package_search { @@ -56,7 +60,7 @@ namespace brep std::string query | q = ""; }; - class package_version_search + class package_details { // Display package version search result list starting from this page. // @@ -65,10 +69,20 @@ namespace brep // Package version search criteria. // std::string query | q = ""; + + // Full page variant. + // + // @@ Shouldn't we use one letter alias for URL as well ? + // I like full. It is descriptive. q= is kind now a convention. + // + bool full = false; }; class package_version_details { + // Full page variant. + // + bool full = false; }; } } diff --git a/brep/package-details b/brep/package-details new file mode 100644 index 0000000..471dcee --- /dev/null +++ b/brep/package-details @@ -0,0 +1,32 @@ +// file : brep/package-details -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BREP_PACKAGE_DETAILS +#define BREP_PACKAGE_DETAILS + +#include // shared_ptr + +#include // database + +#include +#include + +namespace brep +{ + class package_details: public module + { + private: + virtual void + handle (request&, response&); + + virtual void + init (cli::scanner&); + + private: + std::shared_ptr options_; + std::shared_ptr db_; + }; +} + +#endif // BREP_PACKAGE_DETAILS diff --git a/brep/package-details.cxx b/brep/package-details.cxx new file mode 100644 index 0000000..f814ef8 --- /dev/null +++ b/brep/package-details.cxx @@ -0,0 +1,225 @@ +// file : brep/package-details.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include // make_shared(), shared_ptr +#include // size_t + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace cli; +using namespace odb::core; + +namespace brep +{ + void package_details:: + init (scanner& s) + { + MODULE_DIAG; + + options_ = make_shared ( + s, unknown_mode::fail, unknown_mode::fail); + + db_ = shared_database (options_->db_host (), options_->db_port ()); + } + + template + static inline query + search_params (const string& n, const string& q) + { + using query = query; + + return "(" + + (q.empty () + ? query ("NULL") + : "plainto_tsquery (" + query::_val (q) + ")") + + "," + + query::_val (n) + + ")"; + } + + void package_details:: + handle (request& rq, response& rs) + { + using namespace xml; + using namespace web; + using namespace web::xhtml; + + MODULE_DIAG; + + const string& name (*rq.path ().rbegin ()); + params::package_details pr; + + try + { + param_scanner s (rq.parameters ()); + pr = params::package_details (s, unknown_mode::fail, unknown_mode::fail); + } + catch (const unknown_argument& e) + { + throw invalid_request (400, e.what ()); + } + + const string& sq (pr.query ()); // Search query. + size_t pg (pr.page ()); + bool f (pr.full ()); + string en (mime_url_encode (name)); + size_t rp (options_->results_on_page ()); + + auto url ( + [&en](bool f = false, + const string& q = "", + size_t p = 0, + const string& a = "") -> string + { + string s ("?"); + string u (en); + + if (f) { u += "?full"; s = "&"; } + if (!q.empty ()) { u += s + "q=" + mime_url_encode (q); s = "&"; } + if (p > 0) { u += s + "p=" + to_string (p); s = "&"; } + if (!a.empty ()) { u += '#' + a; } + return u; + }); + + serializer s (rs.content (), name); + const string title (sq.empty () ? name : name + " " + sq); + + s << HTML + << HEAD + << TITLE << title << ~TITLE + << CSS_LINKS ("/package-details.css") + << ~HEAD + << BODY + << DIV_HEADER () + << DIV(ID="content"); + + if (f) + s << CLASS << "full" << ~CLASS; + + s << DIV(ID="heading") + << H1 << A(HREF=url ()) << name << ~A << ~H1 + << A(HREF=url (!f, sq, pg)) << (f ? "[brief]" : "[full]") << ~A + << ~DIV; + + transaction t (db_->begin ()); + + shared_ptr p; + { + latest_package lp; + if (!db_->query_one ( + query( + "(" + query::_val (name) + ")"), lp)) + { + throw invalid_request (404, "Package '" + name + "' not found"); + } + + p = db_->load (lp.id); + } + + const license_alternatives& ll (p->license_alternatives); + + if (pg == 0) + { + // Display package details on the first page only. + // + s << H2 << p->summary << ~H2; + + if (const auto& d = p->description) + s << (f + ? P_DESCRIPTION (*d) + : P_DESCRIPTION ( + *d, + options_->description_length (), + url (!f, sq, pg, "description"))); + + s << TABLE(CLASS="proplist", ID="package") + << TBODY + << TR_LICENSE (ll) + << TR_URL (p->url) + << TR_EMAIL (p->email) + << TR_TAGS (p->tags) + << ~TBODY + << ~TABLE; + } + + auto pc ( + db_->query_value ( + search_params (name, sq))); + + auto r ( + db_->query ( + search_params (name, sq) + + "ORDER BY rank DESC, version_epoch DESC, " + "version_canonical_upstream DESC, version_revision DESC" + + "OFFSET" + to_string (pg * rp) + + "LIMIT" + to_string (rp))); + + s << FORM_SEARCH (sq.c_str ()) + << DIV_COUNTER (pc, "Version", "Versions") + + // Enclose the subsequent tables to be able to use nth-child CSS selector. + // + << DIV; + + for (const auto& pr: r) + { + shared_ptr p (db_->load (pr.id)); + + s << TABLE(CLASS="proplist version") + << TBODY + << TR_VERSION (name, p->version.string ()) + + // @@ Shouldn't we skip low priority row ? + // + << TR_PRIORITY (p->priority); + + // Comparing objects of the license_alternatives class as being of the + // vector> class, so comments are not considered. + // + if (p->license_alternatives != ll) + s << TR_LICENSE (p->license_alternatives); + + assert (p->internal_repository != nullptr); + + // @@ Shouldn't we make package location to be a link to the proper + // place of the About page, describing corresponding repository ? + // + // @@ In most cases package location will be the same for all versions + // of the same package. Shouldn't we put package location to the + // package summary part and display it here only if it differes + // from the one in the summary ? + // + s << TR_LOCATION (p->internal_repository.object_id ()) + << TR_DEPENDS (p->dependencies) + << TR_REQUIRES (p->requirements) + << ~TBODY + << ~TABLE; + } + + t.commit (); + + s << ~DIV + << DIV_PAGER (pg, pc, rp, options_->pages_in_pager (), url (f, sq)) + << ~DIV + << ~BODY + << ~HTML; + } +} diff --git a/brep/package-search.cxx b/brep/package-search.cxx index ffe5cb9..64e43f1 100644 --- a/brep/package-search.cxx +++ b/brep/package-search.cxx @@ -73,80 +73,68 @@ namespace brep throw invalid_request (400, e.what ()); } - // @@ Would be nice to have a manipulator indenting string properly - // according to the most nested element identation. - // - const char* ident ("\n "); - const char* title ("Package Search"); - serializer s (rs.content (), title); + const string& sq (pr.query ()); // Search query. + size_t pg (pr.page ()); + string qp (sq.empty () ? "" : "q=" + mime_url_encode (sq)); + size_t rp (options_->results_on_page ()); + + serializer s (rs.content (), "Packages"); + + const string& title ( + sq.empty () ? s.output_name () : s.output_name () + " " + sq); s << HTML << HEAD << TITLE << title << ~TITLE - << CSS_STYLE << ident - << A_STYLE () << ident - << DIV_PAGER_STYLE () << ident - << "#packages {font-size: x-large;}" << ident - << ".package {margin: 0.5em 0 0;}" << ident - << ".name {font-size: x-large;}" << ident - << ".tags {margin: 0.3em 0 0;}" << ident - << "form {margin: 0.5em 0 0 0;}" - << ~CSS_STYLE + << CSS_LINKS ("/package-search.css") << ~HEAD - << BODY; - - const string& sq (pr.query ()); // Search query. - string qp (sq.empty () ? "" : "q=" + mime_url_encode (sq)); - size_t rop (options_->results_on_page ()); + << BODY + << DIV_HEADER () + << DIV(ID="content"); transaction t (db_->begin ()); - size_t pc ( + auto pc ( db_->query_value ( search_param (sq))); - s << DIV(ID="packages") << "Packages (" << pc << ")" << ~DIV - << FORM_SEARCH (sq); - auto r ( db_->query ( search_param (sq) + "ORDER BY rank DESC, name" + - "OFFSET" + to_string (pr.page () * rop) + - "LIMIT" + to_string (rop))); + "OFFSET" + to_string (pg * rp) + + "LIMIT" + to_string (rp))); + + s << FORM_SEARCH (sq.c_str ()) + << DIV_COUNTER (pc, "Package", "Packages") + + // Enclose the subsequent tables to be able to use nth-child CSS selector. + // + << DIV; for (const auto& pr: r) { shared_ptr p (db_->load (pr.id)); - s << DIV(CLASS="package") - << DIV(CLASS="name") - << A - << HREF << "/go/" << mime_url_encode (p->id.name); - - // Propagate search criteria to the package version search url. - // - if (!qp.empty ()) - s << "?" << qp; - - s << ~HREF - << p->id.name - << ~A - << ~DIV - << DIV(CLASS="summary") << p->summary << ~DIV - << DIV_TAGS (p->tags) - << DIV_LICENSES (p->license_alternatives) - << DIV(CLASS="dependencies") - << "Dependencies: " << p->dependencies.size () - << ~DIV - << ~DIV; + s << TABLE(CLASS="proplist package") + << TBODY + << TR_NAME (p->id.name, qp) + << TR_SUMMARY (p->summary) + << TR_LICENSE (p->license_alternatives) + << TR_TAGS (p->tags) + << TR_DEPENDS (p->dependencies) + << TR_REQUIRES (p->requirements) + << ~TBODY + << ~TABLE; } t.commit (); - string u (qp.empty () ? "/" : ("/?" + qp)); + string url (qp.empty () ? "/" : ("/?" + qp)); - s << DIV_PAGER (pr.page (), pc, rop, options_->pages_in_pager (), u) + s << ~DIV + << DIV_PAGER (pg, pc, rp, options_->pages_in_pager (), url) + << ~DIV << ~BODY << ~HTML; } diff --git a/brep/package-version-details.cxx b/brep/package-version-details.cxx index 64945c4..71559b9 100644 --- a/brep/package-version-details.cxx +++ b/brep/package-version-details.cxx @@ -78,39 +78,44 @@ namespace brep throw invalid_request (400, e.what ()); } - const char* ident ("\n "); + bool f (pr.full ()); const string& vs (v.string ()); + + auto url ([&vs](bool f = false, const string& a = "") -> string + { + string u (vs); + + if (f) { u += "?full"; } + if (!a.empty ()) { u += '#' + a; } + return u; + }); + const string name (n + " " + vs); - const string title ("Package Version " + name); - serializer s (rs.content (), title); + serializer s (rs.content (), name); s << HTML << HEAD - << TITLE << title << ~TITLE - << CSS_STYLE << ident - << A_STYLE () << ident - << "#name {font-size: xx-large; font-weight: bold;}" << ident - << ".url, .email {font-size: medium;}" << ident - << ".comment {font-size: small;}" << ident - << "#summary {font-size: x-large; margin: 0.2em 0 0;}" << ident - << "#description {margin: 0.5em 0 0;}" << ident - << ".tags {margin: 0.3em 0 0;}" << ident - << "#package, .priority, #licenses, #dependencies, #requirements, " - "#locations, #changes {" << ident - << " font-size: x-large;" << ident - << " margin: 0.5em 0 0;" << ident - << "}" << ident - << "ul {margin: 0; padding: 0 0 0 1em;}" << ident - << "li {font-size: large; margin: 0.1em 0 0;}" << ident - << ".conditional {font-weight: bold;}" << ident - << "pre {font-size: medium; margin: 0.1em 0 0 1em;}" - << ~CSS_STYLE + << TITLE << name << ~TITLE + << CSS_LINKS ("/package-version-details.css") << ~HEAD << BODY - << DIV(ID="name") - << A << HREF << "/go/" << mime_url_encode (n) << ~HREF << n << ~A - << " " << vs - << ~DIV; + << DIV_HEADER () + << DIV(ID="content"); + + if (f) + s << CLASS << "full" << ~CLASS; + + s << DIV(ID="heading") + << H1 + << A + << HREF << "/go/" << mime_url_encode (n) << ~HREF + << n + << ~A + << "/" + << A(HREF=url ()) << vs << ~A + << ~H1 + << A(HREF=url (!f)) << (f ? "[brief]" : "[full]") << ~A + << ~DIV; bool not_found (false); shared_ptr p; @@ -134,182 +139,138 @@ namespace brep if (not_found) throw invalid_request (404, "Package '" + name + "' not found"); - assert (p->location); - const string u (p->internal_repository.load ()->location.string () + - "/" + p->location->string ()); - - s << DIV(CLASS="url") << A << HREF << u << ~HREF << u << ~A << ~DIV - << DIV(ID="summary") << p->summary << ~DIV - << DIV_URL (p->url) - << DIV_EMAIL (p->email); - - if (p->description) - s << DIV(ID="description") << *p->description << ~DIV; - - const priority& pt (p->priority); + s << H2 << p->summary << ~H2; - s << DIV_TAGS (p->tags) - << DIV_PRIORITY (pt); + if (const auto& d = p->description) + s << (f + ? P_DESCRIPTION (*d) + : P_DESCRIPTION ( + *d, options_->description_length (), url (!f, "description"))); - if (!pt.comment.empty ()) - s << DIV(CLASS="comment") << pt.comment << ~DIV; + // Link to download from the internal repository. + // + assert (p->location); + const string du (p->internal_repository.load ()->location.string () + + "/" + p->location->string ()); - const auto& ls (p->license_alternatives); + t.commit (); - s << DIV(ID="licenses") - << "Licenses:" - << UL; + s << TABLE(CLASS="proplist", ID="version") + << TBODY - for (const auto& la: ls) - { - s << LI; + // Repeat version here since it can be cut out in the header. + // + << TR_VERSION (p->version.string ()) - for (const auto& l: la) - { - if (&l != &la[0]) - s << " & "; + << TR_PRIORITY (p->priority) + << TR_LICENSES (p->license_alternatives) + << TR_LOCATION (p->internal_repository.object_id ()) + << TR_DOWNLOAD (du) + << ~TBODY + << ~TABLE - s << l; - } + << TABLE(CLASS="proplist", ID="package") + << TBODY + << TR_URL (p->url) + << TR_EMAIL (p->email); - if (!la.comment.empty ()) - s << DIV(CLASS="comment") << la.comment << ~DIV; + if (p->package_url && *p->package_url != p->url) + s << TR_URL (*p->package_url, "pkg-url"); - s << ~LI; - } + if (p->package_email && *p->package_email != p->email) + s << TR_EMAIL (*p->package_email, "pkg-email"); - s << ~UL - << ~DIV; + s << TR_TAGS (p->tags) + << ~TBODY + << ~TABLE; const auto& ds (p->dependencies); if (!ds.empty ()) { - s << DIV(ID="dependencies") - << "Dependencies:" - << UL; + s << H3 << "Depends" << ~H3 + << TABLE(CLASS="proplist", ID="depends") + << TBODY; for (const auto& da: ds) { - s << LI; + s << TR(CLASS="depends") + << TH; if (da.conditional) - s << SPAN(CLASS="conditional") << "? " << ~SPAN; + s << "?"; + + s << ~TH + << TD + << SPAN(CLASS="value"); for (const auto& d: da) { if (&d != &da[0]) s << " | "; - // @@ Should it be a link to the package version search page or - // the best matching package version details page on the - // corresponding repository site ? - // - s << d; + s << d; // @@ Should it be a link ? } - if (!da.comment.empty ()) - s << DIV(CLASS="comment") << da.comment << ~DIV; - - s << ~LI; + s << ~SPAN + << SPAN_COMMENT (da.comment) + << ~TD + << ~TR; } - s << ~UL - << ~DIV; + s << ~TBODY + << ~TABLE; } - const auto& rm (p->requirements); + const auto& rt (p->requirements); - if (!rm.empty ()) + if (!rt.empty ()) { - s << DIV(ID="requirements") - << "Requirements:" - << UL; + s << H3 << "Requires" << ~H3 + << TABLE(CLASS="proplist", ID="requires") + << TBODY; - for (const auto& ra: rm) + for (const auto& ra: rt) { - s << LI; + s << TR(CLASS="requires") + << TH; if (ra.conditional) - s << SPAN(CLASS="conditional") << "? " << ~SPAN; - - if (ra.empty ()) - // If there is no requirement alternatives specified, then - // print the comment instead. - // - s << ra.comment; - else - { - for (const auto& r: ra) - { - if (&r != &ra[0]) - s << " | "; - - s << r; - } - - if (!ra.comment.empty ()) - s << DIV(CLASS="comment") << ra.comment << ~DIV; - } - - s << ~LI; - } - - s << ~UL - << ~DIV; - } - - if (p->package_url || p->package_email) - { - s << DIV(ID="package") - << "Package:" - << UL; - - if (p->package_url) - s << LI << DIV_URL (*p->package_url) << ~LI; - - if (p->package_email) - s << LI << DIV_EMAIL (*p->package_email) << ~LI; - - s << ~UL - << ~DIV; - } - - const auto& er (p->external_repositories); + s << "?"; - if (!er.empty ()) - { - s << DIV(ID="locations") - << "Alternative Locations:" - << UL; + s << ~TH + << TD + << SPAN(CLASS="value"); - for (const auto& r: er) - { - repository_location l (move (r.load ()->location)); - assert (l.remote ()); + for (const auto& r: ra) + { + if (&r != &ra[0]) + s << " | "; - string u ("http://" + l.host ()); - if (l.port () != 0) - u += ":" + to_string (l.port ()); + s << r; + } - u += "/go/" + mime_url_encode (n) + "/" + vs; - s << LI - << DIV(CLASS="url") << A << HREF << u << ~HREF << u << ~A << ~DIV - << ~LI; + s << ~SPAN + << SPAN_COMMENT (ra.comment) + << ~TD + << ~TR; } - s << ~UL - << ~DIV; + s << ~TBODY + << ~TABLE; } - t.commit (); - const string& ch (p->changes); if (!ch.empty ()) - s << DIV(ID="changes") << "Changes:" << PRE << ch << ~PRE << ~DIV; - - s << ~BODY + s << H3 << "Changes" << ~H3 + << (f + ? PRE_CHANGES (ch) + : PRE_CHANGES ( + ch, options_->changes_length (), url (!f, "changes"))); + + s << ~DIV + << ~BODY << ~HTML; } } diff --git a/brep/package-version-search b/brep/package-version-search deleted file mode 100644 index 0292ae0..0000000 --- a/brep/package-version-search +++ /dev/null @@ -1,32 +0,0 @@ -// file : brep/package-version-search -*- C++ -*- -// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#ifndef BREP_PACKAGE_VERSION_SEARCH -#define BREP_PACKAGE_VERSION_SEARCH - -#include // shared_ptr - -#include // database - -#include -#include - -namespace brep -{ - class package_version_search: public module - { - private: - virtual void - handle (request&, response&); - - virtual void - init (cli::scanner&); - - private: - std::shared_ptr options_; - std::shared_ptr db_; - }; -} - -#endif // BREP_PACKAGE_VERSION_SEARCH diff --git a/brep/package-version-search.cxx b/brep/package-version-search.cxx deleted file mode 100644 index 84c06e1..0000000 --- a/brep/package-version-search.cxx +++ /dev/null @@ -1,179 +0,0 @@ -// file : brep/package-version-search.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include - -#include -#include // make_shared(), shared_ptr -#include // size_t - -#include - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace std; -using namespace cli; -using namespace odb::core; - -namespace brep -{ - void package_version_search:: - init (scanner& s) - { - MODULE_DIAG; - - options_ = make_shared ( - s, unknown_mode::fail, unknown_mode::fail); - - db_ = shared_database (options_->db_host (), options_->db_port ()); - } - - template - static inline query - search_params (const string& n, const string& q) - { - using query = query; - - return "(" + - (q.empty () - ? query ("NULL") - : "plainto_tsquery (" + query::_val (q) + ")") + - "," + - query::_val (n) + - ")"; - } - - void package_version_search:: - handle (request& rq, response& rs) - { - using namespace xml; - using namespace web; - using namespace web::xhtml; - - MODULE_DIAG; - - const string& name (*rq.path ().rbegin ()); - params::package_version_search pr; - - try - { - param_scanner s (rq.parameters ()); - pr = params::package_version_search ( - s, unknown_mode::fail, unknown_mode::fail); - } - catch (const unknown_argument& e) - { - throw invalid_request (400, e.what ()); - } - - const char* ident ("\n "); - const string title ("Package " + name); - serializer s (rs.content (), title); - - s << HTML - << HEAD - << TITLE << title << ~TITLE - << CSS_STYLE << ident - << A_STYLE () << ident - << DIV_PAGER_STYLE () << ident - << "#name {font-size: xx-large; font-weight: bold;}" << ident - << "#summary {font-size: x-large; margin: 0.2em 0 0;}" << ident - << ".url, .email {font-size: medium;}" << ident - << ".comment {font-size: small;}" << ident - << "#description {margin: 0.5em 0 0;}" << ident - << ".tags {margin: 0.3em 0 0;}" << ident - << "#versions {font-size: x-large; margin: 0.5em 0 0;}" << ident - << ".package_version {margin: 0.5em 0 0;}" << ident - << ".version {font-size: x-large;}" << ident - << ".priority {margin: 0.3em 0 0;}" << ident - << "form {margin: 0.5em 0 0 0;}" - << ~CSS_STYLE - << ~HEAD - << BODY - << DIV(ID="name") << name << ~DIV; - - const string& sq (pr.query ()); // Search query. - size_t rop (options_->results_on_page ()); - - transaction t (db_->begin ()); - - shared_ptr p; - { - latest_package lp; - if (!db_->query_one ( - query( - "(" + query::_val (name) + ")"), lp)) - { - throw invalid_request (404, "Package '" + name + "' not found"); - } - - p = db_->load (lp.id); - } - - s << DIV(ID="summary") << p->summary << ~DIV - << DIV_URL (p->url) - << DIV_EMAIL (p->email); - - if (p->description) - s << DIV(ID="description") << *p->description << ~DIV; - - s << DIV_TAGS (p->tags); - - size_t pvc ( - db_->query_value ( - search_params (name, sq))); - - s << DIV(ID="versions") << "Versions (" << pvc << ")" << ~DIV - << FORM_SEARCH (sq); - - auto r ( - db_->query ( - search_params (name, sq) + - "ORDER BY rank DESC, version_epoch DESC, " - "version_canonical_upstream DESC, version_revision DESC" + - "OFFSET" + to_string (pr.page () * rop) + - "LIMIT" + to_string (rop))); - - for (const auto& pr: r) - { - shared_ptr p (db_->load (pr.id)); - const string& v (p->version.string ()); - - s << DIV(CLASS="package_version") - << DIV(CLASS="version") - << A - << HREF << "/go/" << mime_url_encode (name) << "/" << v << ~HREF - << v - << ~A - << ~DIV - << DIV_PRIORITY (p->priority) - << DIV_LICENSES (p->license_alternatives) - << DIV(CLASS="dependencies") - << "Dependencies: " << p->dependencies.size () - << ~DIV - << ~DIV; - } - - t.commit (); - - string u (mime_url_encode (name)); - if (!sq.empty ()) - u += "?q=" + mime_url_encode (sq); - - s << DIV_PAGER (pr.page (), pvc, rop, options_->pages_in_pager (), u) - << ~BODY - << ~HTML; - } -} diff --git a/brep/page b/brep/page index b923c7f..309caa9 100644 --- a/brep/page +++ b/brep/page @@ -17,78 +17,150 @@ namespace brep // Page common building blocks. // - // A element default style. + // Generates CSS link elements. // - struct A_STYLE + class CSS_LINKS { + public: + CSS_LINKS (const char* u): url_ (u) {} + void operator() (xml::serializer& s) const; + + private: + const char* url_; }; - // Generates paging element. + // Generates page header element. // - class DIV_PAGER + struct DIV_HEADER + { + void + operator() (xml::serializer& s) const; + }; + + // Generates package search form element. + // + class FORM_SEARCH { public: - DIV_PAGER (std::size_t current_page, - std::size_t item_count, - std::size_t item_per_page, - std::size_t page_number_count, - const std::string& url); + FORM_SEARCH (const std::string& q): query_ (q) {} void operator() (xml::serializer& s) const; private: - std::size_t current_page_; - std::size_t item_count_; - std::size_t item_per_page_; - std::size_t page_number_count_; - const std::string& url_; + const std::string& query_; }; - // DIV_PAGER element default style. + // Generates 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 + // look that ugly. // - struct DIV_PAGER_STYLE + class DIV_COUNTER { + public: + DIV_COUNTER (std::size_t c, const char* s, const char* p) + : count_ (c), singular_ (s), plural_ (p) {} + void operator() (xml::serializer& s) const; + + private: + std::size_t count_; + const char* singular_; + const char* plural_; }; - // Generates url element. + // Generates package name element. // - class DIV_URL + class TR_NAME { public: - DIV_URL (const url& u): url_ (u) {} + TR_NAME (const std::string& n, const std::string& q) + : name_ (n), query_param_ (q) {} void operator() (xml::serializer& s) const; private: - const url& url_; + const std::string& name_; + const std::string& query_param_; }; - // Generates email element. + // Generates package version element. // - class DIV_EMAIL + class TR_VERSION { public: - DIV_EMAIL (const email& e): email_ (e) {} + // Display the version as a link to the package version details page. + // + TR_VERSION (const std::string& p, const std::string& v) + : package_ (&p), version_ (v) {} + + // Display the version as a regular text. + // + TR_VERSION (const std::string& v): package_ (nullptr), version_ (v) {} void operator() (xml::serializer& s) const; private: - const email& email_; + const std::string* package_; + const std::string& version_; + }; + + // Generates package summary element. + // + class TR_SUMMARY + { + public: + TR_SUMMARY (const std::string& s): summary_ (s) {} + + void + operator() (xml::serializer& s) const; + + private: + const std::string& summary_; + }; + + // Generates package license alternatives element. + // + class TR_LICENSE + { + public: + TR_LICENSE (const license_alternatives& l): licenses_ (l) {} + + void + operator() (xml::serializer& s) const; + + private: + const license_alternatives& licenses_; + }; + + // Generates package license alternatives elements. Differs from TR_LICENSE + // by producing multiple rows instead of a single one. + // + class TR_LICENSES + { + public: + TR_LICENSES (const license_alternatives& l): licenses_ (l) {} + + void + operator() (xml::serializer& s) const; + + private: + const license_alternatives& licenses_; }; // Generates package tags element. // - class DIV_TAGS + class TR_TAGS { public: - DIV_TAGS (const strings& ts): tags_ (ts) {} + TR_TAGS (const strings& ts): tags_ (ts) {} void operator() (xml::serializer& s) const; @@ -97,26 +169,71 @@ namespace brep const strings& tags_; }; - // Generates package version license alternatives element. + // Generates package dependencies element. // - class DIV_LICENSES + class TR_DEPENDS { public: - DIV_LICENSES (const license_alternatives& l): license_alternatives_ (l) {} + TR_DEPENDS (const dependencies& d): dependencies_ (d) {} void operator() (xml::serializer& s) const; private: - const license_alternatives& license_alternatives_; + const dependencies& dependencies_; + }; + + // Generates package requirements element. + // + class TR_REQUIRES + { + public: + TR_REQUIRES (const requirements& r): requirements_ (r) {} + + void + operator() (xml::serializer& s) const; + + private: + const requirements& requirements_; + }; + + // Generates url element. + // + class TR_URL + { + public: + TR_URL (const url& u, const char* l = "url"): url_ (u), label_ (l) {} + + void + operator() (xml::serializer& s) const; + + private: + const url& url_; + const char* label_; + }; + + // Generates email element. + // + class TR_EMAIL + { + public: + TR_EMAIL (const email& e, const char* l = "email") + : email_ (e), label_ (l) {} + + void + operator() (xml::serializer& s) const; + + private: + const email& email_; + const char* label_; }; // Generates package version priority element. // - class DIV_PRIORITY + class TR_PRIORITY { public: - DIV_PRIORITY (const priority& p): priority_ (p) {} + TR_PRIORITY (const priority& p): priority_ (p) {} void operator() (xml::serializer& s) const; @@ -125,18 +242,116 @@ namespace brep const priority& priority_; }; - // Generates package search element. + // Generates package location element. // - class FORM_SEARCH + class TR_LOCATION { public: - FORM_SEARCH (const std::string& q): query_ (q) {} + TR_LOCATION (const std::string& l): location_ (l) {} void operator() (xml::serializer& s) const; private: - const std::string& query_; + const std::string& location_; + }; + + // Generates package download URL element. + // + class TR_DOWNLOAD + { + public: + TR_DOWNLOAD (const std::string& u): url_ (u) {} + + void + operator() (xml::serializer& s) const; + + private: + const std::string& url_; + }; + + // Generates comment element. + // + class SPAN_COMMENT + { + public: + SPAN_COMMENT (const std::string& c): comment_ (c) {} + + void + operator() (xml::serializer& s) const; + + private: + const std::string& comment_; + }; + + // Generates package description element. + // + class P_DESCRIPTION + { + public: + // Genereate full description. + // + P_DESCRIPTION (const std::string& d) + : description_ (d), length_ (d.size ()), url_ (nullptr) {} + + // Genereate brief description. + // + P_DESCRIPTION (const std::string& d, size_t l, const std::string& u) + : description_ (d), length_ (l), url_ (&u) {} + + void + operator() (xml::serializer& s) const; + + private: + const std::string& description_; + std::size_t length_; + const std::string* url_; // Full page url. + }; + + // Generates package description element. + // + class PRE_CHANGES + { + public: + // Genereate full changes info. + // + PRE_CHANGES (const std::string& c) + : changes_ (c), length_ (c.size ()), url_ (nullptr) {} + + // Genereate brief changes info. + // + PRE_CHANGES (const std::string& c, size_t l, const std::string& u) + : changes_ (c), length_ (l), url_ (&u) {} + + void + operator() (xml::serializer& s) const; + + private: + const std::string& changes_; + std::size_t length_; + const std::string* url_; // Full page url. + }; + + // Generates paging element. + // + class DIV_PAGER + { + public: + DIV_PAGER (std::size_t current_page, + std::size_t item_count, + std::size_t item_per_page, + std::size_t page_number_count, + const std::string& url); + + void + operator() (xml::serializer& s) const; + + private: + std::size_t current_page_; + std::size_t item_count_; + std::size_t item_per_page_; + std::size_t page_number_count_; + const std::string& url_; }; } diff --git a/brep/page.cxx b/brep/page.cxx index c68a7eb..a23ce17 100644 --- a/brep/page.cxx +++ b/brep/page.cxx @@ -4,6 +4,7 @@ #include +#include #include #include #include // move() @@ -12,209 +13,576 @@ #include #include +#include #include using namespace std; using namespace xml; +using namespace web; using namespace web::xhtml; namespace brep { - // A_STYLE + // CSS_LINKS // - void A_STYLE:: - operator() (xml::serializer& s) const + void CSS_LINKS:: + operator() (serializer& s) const { - const char* ident ("\n "); - s << "a {text-decoration: none;}" << ident - << "a:hover {text-decoration: underline;}"; + s << *LINK(REL="stylesheet", TYPE="text/css", HREF="/common.css") + << *LINK(REL="stylesheet", TYPE="text/css", HREF=url_); } - // DIV_PAGER + // DIV_HEADER // - DIV_PAGER:: - DIV_PAGER (size_t current_page, - size_t item_count, - size_t item_per_page, - size_t page_number_count, - const string& url) - : current_page_ (current_page), - item_count_ (item_count), - item_per_page_ (item_per_page), - page_number_count_ (page_number_count), - url_ (url) + void DIV_HEADER:: + operator() (serializer& s) const { + s << DIV(ID="header") + << DIV(ID="header-menu") + << A(HREF="/") << "packages" << ~A + << A(HREF="/about") << "about" << ~A + << ~DIV + << ~DIV; } - void DIV_PAGER:: + // FORM_SEARCH + // + void FORM_SEARCH:: operator() (serializer& s) const { - if (item_count_ == 0 || item_per_page_ == 0) - return; + // The 'action' attribute is optional in HTML5. While the standard don't + // specify browser behavior explicitly for the case the attribute is + // ommited, the only reasonable behavior is to default it to the current + // document URL. + // + s << FORM(ID="search") + << TABLE(CLASS="form-table") + << TBODY + << TR + << TD(ID="search-txt") + << *INPUT(TYPE="search", NAME="q", VALUE=query_, + AUTOFOCUS="autofocus") + << ~TD + << TD(ID="search-btn") + << *INPUT(TYPE="submit", VALUE="Search") + << ~TD + << ~TR + << ~TBODY + << ~TABLE + << ~FORM; + } - size_t pc (item_count_ / item_per_page_); // Page count. + // DIV_COUNTER + // + void DIV_COUNTER:: + operator() (serializer& s) const + { + s << DIV(ID="count") + << count_ << " " + << (count_ % 10 == 1 && count_ % 100 != 11 ? singular_ : plural_) + << ~DIV; + } - if (item_count_ % item_per_page_) - ++pc; + // TR_NAME + // + void TR_NAME:: + operator() (serializer& s) const + { + s << TR(CLASS="name") + << TH << "name" << ~TH + << TD + << SPAN(CLASS="value") + << A + << HREF + << "/go/" << mime_url_encode (name_); + + // Propagate search criteria to the package details page. + // + if (!query_param_.empty ()) + s << "?" << query_param_; + + s << ~HREF + << name_ + << ~A + << ~SPAN + << ~TD + << ~TR; + } - if (pc > 1) - { - auto u ( - [this](size_t page) -> string - { - return page == 0 - ? url_ - : url_ + (url_.find ('?') == string::npos ? "?p=" : "&p=") + - to_string (page); - }); + void TR_VERSION:: + operator() (serializer& s) const + { + s << TR(CLASS="version") + << TH << "version" << ~TH + << TD + << SPAN(CLASS="value"); + + if (package_ == nullptr) + s << version_; + else + s << A + << HREF + << "/go/" << mime_url_encode (*package_) << "/" << version_ + << ~HREF + << version_ + << ~A; + + s << ~SPAN + << ~TD + << ~TR; + } - // Can consider customizing class names if use-case appear. - // - s << DIV(CLASS="pager"); + // TR_SUMMARY + // + void TR_SUMMARY:: + operator() (serializer& s) const + { + s << TR(CLASS="summary") + << TH << "summary" << ~TH + << TD << SPAN(CLASS="value") << summary_ << ~SPAN << ~TD + << ~TR; + } - if (current_page_ > 0) - s << A(CLASS="pg-prev") - << HREF << u (current_page_ - 1) << ~HREF - << "<<" - << ~A - << " "; + // TR_LICENSE + // + void TR_LICENSE:: + operator() (serializer& s) const + { + s << TR(CLASS="license") + << TH << "license" << ~TH + << TD + << SPAN(CLASS="value"); - if (page_number_count_) + for (const auto& la: licenses_) { - size_t offset (page_number_count_ / 2); - size_t fp (current_page_ > offset ? current_page_ - offset : 0); - size_t tp (min (fp + page_number_count_, pc)); + if (&la != &licenses_[0]) + s << " " << EM << "or" << ~EM << " "; - for (size_t p (fp); p < tp; ++p) + bool m (la.size () > 1); + + if (m) + s << "("; + + for (const auto& l: la) { - if (p == current_page_) - s << SPAN(CLASS="pg-cpage") << p + 1 << ~SPAN; - else - s << A(CLASS="pg-page") - << HREF << u (p) << ~HREF - << p + 1 - << ~A; + if (&l != &la[0]) + s << " " << EM << "and" << ~EM << " "; - s << " "; + s << l; } + + if (m) + s << ")"; } - if (current_page_ < pc - 1) - s << A(CLASS="pg-next") - << HREF << u (current_page_ + 1) << ~HREF - << ">>" - << ~A; + s << ~SPAN + << ~TD + << ~TR; + } - s << ~DIV; + // TR_LICENSES + // + void TR_LICENSES:: + operator() (serializer& s) const + { + for (const auto& la: licenses_) + { + s << TR(CLASS="license") + << TH << "license" << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& l: la) + { + if (&l != &la[0]) + s << " " << EM << "and" << ~EM << " "; + + s << l; + } + + s << ~SPAN + << SPAN_COMMENT (la.comment) + << ~TD + << ~TR; } } - // DIV_PAGER_STYLE + // TR_TAGS // - void DIV_PAGER_STYLE:: - operator() (xml::serializer& s) const + void TR_TAGS:: + operator() (serializer& s) const { - const char* ident ("\n "); - s << ".pager {margin: 0.5em 0 0;}" << ident - << ".pg-prev {padding: 0 0.3em 0 0;}" << ident - << ".pg-page {padding: 0 0.3em 0 0;}" << ident - << ".pg-cpage {padding: 0 0.3em 0 0; font-weight: bold;}"; + if (!tags_.empty ()) + { + s << TR(CLASS="tags") + << TH << "tags" << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& t: tags_) + { + if (&t != &tags_[0]) + s << " "; + + s << A << HREF << "/?q=" << mime_url_encode (t) << ~HREF << t << ~A; + } + + s << ~SPAN + << ~TD + << ~TR; + } } - // DIV_LICENSES + // TR_DEPENDS // - void DIV_LICENSES:: + void TR_DEPENDS:: operator() (serializer& s) const { - s << DIV(CLASS="licenses") - << "Licenses: "; + s << TR(CLASS="depends") + << TH << "depends" << ~TH + << TD + << SPAN(CLASS="value") + << dependencies_.size (); - for (const auto& la: license_alternatives_) + if (!dependencies_.empty ()) + s << "; "; + + for (const auto& d: dependencies_) { - if (&la != &license_alternatives_[0]) - s << " | "; + if (&d != &dependencies_[0]) + s << ", "; - for (const auto& l: la) + if (d.conditional) + s << "?"; + + // Suppress package name duplicates. + // + set ds; + for (const auto& da: d) + ds.emplace (da.name); + + bool m (ds.size () > 1); + + if (m) + s << "("; + + bool first (true); + for (const auto& da: d) { - if (&l != &la[0]) - s << " & "; + if (ds.find (da.name) != ds.end ()) + { + ds.erase (da.name); - s << l; + if (first) + first = false; + else + s << " | "; + + s << da.name; // @@ Make it a link. + } } + + if (m) + s << ")"; } - s << ~DIV; + s << ~SPAN + << ~TD + << ~TR; } - // DIV_URL + // TR_REQUIRES // - void DIV_URL:: + void TR_REQUIRES:: operator() (serializer& s) const { - s << DIV(CLASS="url") - << A << HREF << url_ << ~HREF << url_ << ~A; + // If there are no requirements, then we omit it, unlike depends, where we + // show 0 explicitly. + // + if (requirements_.empty ()) + return; + + s << TR(CLASS="requires") + << TH << "requires" << ~TH + << TD + << SPAN(CLASS="value") + << requirements_.size () << "; "; + + for (const auto& r: requirements_) + { + if (&r != &requirements_[0]) + s << ", "; - if (!url_.comment.empty ()) - s << DIV(CLASS="comment") << url_.comment << ~DIV; + if (r.conditional) + s << "?"; + + if (r.empty ()) + { + // If there is no requirement alternatives specified, then + // print the comment first word. + // + const auto& c (r.comment); + if (!c.empty ()) + { + auto n (c.find (' ')); + s << string (c, 0, n); + + if (n != string::npos) + s << "..."; + } + } + else + { + bool m (r.size () > 1); - s << ~DIV; + if (m) + s << "("; + + for (const auto& ra: r) + { + if (&ra != &r[0]) + s << " | "; + + s << ra; + } + + if (m) + s << ")"; + } + } + + s << ~SPAN + << ~TD + << ~TR; + } + + // TR_URL + // + void TR_URL:: + operator() (serializer& s) const + { + s << TR(CLASS=label_) + << TH << label_ << ~TH + << TD + << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN + << SPAN_COMMENT (url_.comment) + << ~TD + << ~TR; + } + + // TR_EMAIL + // + void TR_EMAIL:: + operator() (serializer& s) const + { + s << TR(CLASS=label_) + << TH << label_ << ~TH + << TD + << SPAN(CLASS="value") + << A << HREF << "mailto:" << email_ << ~HREF << email_ << ~A + << ~SPAN + << SPAN_COMMENT (email_.comment) + << ~TD + << ~TR; } - // DIV_EMAIL + // TR_PRIORITY // - void DIV_EMAIL:: + void TR_PRIORITY:: operator() (serializer& s) const { - s << DIV(CLASS="email") - << A << HREF << "mailto:" << email_ << ~HREF << email_ << ~A; + static const strings priority_names ({"low", "medium", "high", "security"}); + assert (priority_ < priority_names.size ()); - if (!email_.comment.empty ()) - s << DIV(CLASS="comment") << email_.comment << ~DIV; + s << TR(CLASS="priority") + << TH << "priority" << ~TH + << TD + << SPAN(CLASS="value") << priority_names[priority_] << ~SPAN + << SPAN_COMMENT (priority_.comment) + << ~TD + << ~TR; + } - s << ~DIV; + // TR_LOCATION + // + void TR_LOCATION:: + operator() (serializer& s) const + { + s << TR(CLASS="location") + << TH << "location" << ~TH + << TD << SPAN(CLASS="value") << location_ << ~SPAN << ~TD + << ~TR; } - // DIV_TAGS + // TR_DOWNLOAD // - void DIV_TAGS:: + void TR_DOWNLOAD:: operator() (serializer& s) const { - if (!tags_.empty ()) + s << TR(CLASS="download") + << TH << "download" << ~TH + << TD + << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN + << ~TD + << ~TR; + } + + // SPAN_COMMENT + // + void SPAN_COMMENT:: + operator() (serializer& s) const + { + if (size_t l = comment_.size ()) + s << SPAN(CLASS="comment") + << (comment_[l - 1] == '.' ? string (comment_, 0, l - 1) : comment_) + << ~SPAN; + } + + // P_DESCRIPTION + // + void P_DESCRIPTION:: + operator() (serializer& s) const + { + if (description_.empty ()) + return; + + string::size_type n (description_.find_first_of (" \t\n", length_)); + bool f (n == string::npos); // Description length is below the limit. + + // Truncate description if length exceed the limit. + // + const string& d (f ? description_ : string (description_, 0, n)); + + // Format the description into paragraphs, recognizing a blank line as + // paragraph separator, and replacing single newlines with a space. + // + s << P(ID="description"); + + bool nl (false); // The previous character is '\n'. + for (const auto& c: d) { - s << DIV(CLASS="tags") - << "Tags: "; + if (c == '\n') + { + if (nl) + { + s << ~P << P; + nl = false; + } + else + nl = true; // Delay printing until the next character. + } + else + { + if (nl) + { + s << ' '; // Replace the previous newline with a space. + nl = false; + } - for (const auto& t: tags_) - s << t << " "; + s << c; + } + } - s << ~DIV; + if (!f) + { + assert (url_ != nullptr); + s << "... " << A(HREF=*url_) << "More" << ~A; } + + s << ~P; } - // DIV_PRIORITY + // PRE_CHANGES // - void DIV_PRIORITY:: + void PRE_CHANGES:: operator() (serializer& s) const { - static const strings priority_names ( - {"low", "medium", "high", "security"}); + if (changes_.empty ()) + return; - assert (priority_ < priority_names.size ()); + string::size_type n (changes_.find_first_of (" \t\n", length_)); + bool f (n == string::npos); // Changes length is below the limit. - s << DIV(CLASS="priority") - << "Priority: " << priority_names[priority_] - << ~DIV; + // Truncate changes if length exceed the limit. + // + const string& c (f ? changes_ : string (changes_, 0, n)); + s << PRE(ID="changes") << c; + + if (!f) + { + assert (url_ != nullptr); + s << "... " << A(HREF=*url_) << "More" << ~A; + } + + s << ~PRE; } - // FORM_SEARCH + // DIV_PAGER // - void FORM_SEARCH:: + DIV_PAGER:: + DIV_PAGER (size_t current_page, + size_t item_count, + size_t item_per_page, + size_t page_number_count, + const string& url) + : current_page_ (current_page), + item_count_ (item_count), + item_per_page_ (item_per_page), + page_number_count_ (page_number_count), + url_ (url) + { + } + + void DIV_PAGER:: operator() (serializer& s) const { - s << FORM - << *INPUT(TYPE="search", NAME="q", VALUE=query_) - << *INPUT(TYPE="submit", VALUE="Search") - << ~FORM; + if (item_count_ == 0 || item_per_page_ == 0) + return; + + size_t pc (item_count_ / item_per_page_); // Page count. + + if (item_count_ % item_per_page_) + ++pc; + + if (pc > 1) + { + auto u ( + [this](size_t page) -> string + { + return page == 0 + ? url_ + : url_ + (url_.find ('?') == string::npos ? "?p=" : "&p=") + + to_string (page); + }); + + s << DIV(ID="pager"); + + if (current_page_ > 0) + s << A(ID="prev", HREF=u (current_page_ - 1)) << "Prev" << ~A; + + if (page_number_count_) + { + size_t offset (page_number_count_ / 2); + size_t fp (current_page_ > offset ? current_page_ - offset : 0); + size_t tp (min (fp + page_number_count_, pc)); + + for (size_t p (fp); p < tp; ++p) + { + s << A(HREF=u (p)); + + if (p == current_page_) + s << ID << "curr" << ~ID; + + s << p + 1 + << ~A; + } + } + + if (current_page_ < pc - 1) + s << A(ID="next", HREF=u (current_page_ + 1)) << "Next" << ~A; + + s << ~DIV; + } } } diff --git a/brep/services.cxx b/brep/services.cxx index c022a7d..eb99f3d 100644 --- a/brep/services.cxx +++ b/brep/services.cxx @@ -7,7 +7,7 @@ #include #include -#include +#include #include using namespace brep; @@ -19,10 +19,10 @@ service AP_MODULE_DECLARE_DATA package_search_srv ( package_search_mod, {"db-host", "db-port", "conf"}); -static package_version_search package_version_search_mod; -service AP_MODULE_DECLARE_DATA package_version_search_srv ( - "package-version-search", - package_version_search_mod, +static package_details package_details_mod; +service AP_MODULE_DECLARE_DATA package_details_srv ( + "package-details", + package_details_mod, {"db-host", "db-port", "conf"}); static package_version_details package_version_details_mod; -- cgit v1.1