Skip to content

Commit

Permalink
separated hyperlink functionality from HtmlSpecialHandler to Hyperlin…
Browse files Browse the repository at this point in the history
…kManager
  • Loading branch information
mgieseki committed Oct 11, 2017
1 parent af29e6a commit 8c8cb81
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 259 deletions.
234 changes: 10 additions & 224 deletions src/HtmlSpecialHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,13 @@
** along with this program; if not, see <http://www.gnu.org/licenses/>. **
*************************************************************************/

#include <cassert>
#include <sstream>
#include "HtmlSpecialHandler.hpp"
#include "HyperlinkManager.hpp"
#include "InputReader.hpp"
#include "Message.hpp"
#include "SpecialActions.hpp"
#include "SVGTree.hpp"

using namespace std;

// variable to select the link marker variant (none, underlined, boxed, or colored background)
HtmlSpecialHandler::MarkerType HtmlSpecialHandler::MARKER_TYPE = HtmlSpecialHandler::MarkerType::LINE;
Color HtmlSpecialHandler::LINK_BGCOLOR;
Color HtmlSpecialHandler::LINK_LINECOLOR;
bool HtmlSpecialHandler::USE_LINECOLOR = false;


void HtmlSpecialHandler::preprocess (const char *, istream &is, SpecialActions &actions) {
StreamInputReader ir(is);
Expand All @@ -43,38 +34,9 @@ void HtmlSpecialHandler::preprocess (const char *, istream &is, SpecialActions &
if (ir.check("<a ") && ir.parseAttributes(attribs, '"') > 0) {
unordered_map<string,string>::iterator it;
if ((it = attribs.find("name")) != attribs.end())
preprocessNameAnchor(it->second, actions);
HyperlinkManager::instance().addNameAchor(it->second, actions.getCurrentPageNumber());
else if ((it = attribs.find("href")) != attribs.end())
preprocessHrefAnchor(it->second);
}
}


void HtmlSpecialHandler::preprocessNameAnchor (const string &name, SpecialActions &actions) {
auto it = _namedAnchors.find(name);
if (it == _namedAnchors.end()) { // anchor completely undefined?
int id = static_cast<int>(_namedAnchors.size())+1;
_namedAnchors[name] = NamedAnchor(actions.getCurrentPageNumber(), id, 0);
}
else if (it->second.id < 0) { // anchor referenced but not defined yet?
it->second.id *= -1;
it->second.pageno = actions.getCurrentPageNumber();
}
else
Message::wstream(true) << "named hyperref anchor '" << name << "' redefined\n";
}


void HtmlSpecialHandler::preprocessHrefAnchor (const string &uri) {
if (uri[0] != '#')
return;
string name = uri.substr(1);
NamedAnchors::iterator it = _namedAnchors.find(name);
if (it != _namedAnchors.end()) // anchor already defined?
it->second.referenced = true;
else {
int id = static_cast<int>(_namedAnchors.size())+1;
_namedAnchors[name] = NamedAnchor(0, -id, 0, true);
HyperlinkManager::instance().addHrefAnchor(it->second);
}
}

Expand All @@ -87,213 +49,37 @@ bool HtmlSpecialHandler::process (const char *, istream &is, SpecialActions &act
unordered_map<string,string>::iterator it;
if (ir.check("<a ") && ir.parseAttributes(attribs, '"') > 0) {
if ((it = attribs.find("href")) != attribs.end()) // <a href="URI">
processHrefAnchor(it->second, actions);
HyperlinkManager::instance().createLink(it->second, actions);
else if ((it = attribs.find("name")) != attribs.end()) // <a name="ID">
processNameAnchor(it->second, actions);
HyperlinkManager::instance().setActiveNameAnchor(it->second, actions);
else
return false; // none or only invalid attributes
}
else if (ir.check("</a>"))
closeAnchor(actions);
HyperlinkManager::instance().closeAnchor(actions);
else if (ir.check("<img src=")) {
}
else if (ir.check("<base ") && ir.parseAttributes(attribs, '"') > 0 && (it = attribs.find("href")) != attribs.end())
_base = it->second;
HyperlinkManager::instance().setBaseUrl(it->second);
return true;
}


/** Handles anchors with href attribute: <a href="URI">...</a>
* @param uri value of href attribute */
void HtmlSpecialHandler::processHrefAnchor (string uri, SpecialActions &actions) {
closeAnchor(actions);
string name;
if (uri[0] == '#') { // reference to named anchor?
name = uri.substr(1);
NamedAnchors::iterator it = _namedAnchors.find(name);
if (it == _namedAnchors.end() || it->second.id < 0)
Message::wstream(true) << "reference to undefined anchor '" << name << "'\n";
else {
int id = it->second.id;
uri = "#loc"+XMLString(id);
if (actions.getCurrentPageNumber() != it->second.pageno) {
ostringstream oss;
oss << actions.getSVGFilename(it->second.pageno) << uri;
uri = oss.str();
}
}
}
if (!_base.empty() && uri.find("://") != string::npos) {
if (*_base.rbegin() != '/' && uri[0] != '/' && uri[0] != '#')
uri = "/" + uri;
uri = _base + uri;
}
XMLElementNode *anchor = new XMLElementNode("a");
anchor->addAttribute("xlink:href", uri);
anchor->addAttribute("xlink:title", XMLString(name.empty() ? uri : name, false));
actions.pushContextElement(anchor);
actions.bbox("{anchor}", true); // start computing the bounding box of the linked area
_depthThreshold = actions.getDVIStackDepth();
_anchorType = AnchorType::HREF;
}


/** Handles anchors with name attribute: <a name="NAME">...</a>
* @param name value of name attribute */
void HtmlSpecialHandler::processNameAnchor (const string &name, SpecialActions &actions) {
closeAnchor(actions);
NamedAnchors::iterator it = _namedAnchors.find(name);
assert(it != _namedAnchors.end());
it->second.pos = actions.getY();
_anchorType = AnchorType::NAME;
}


/** Handles the closing tag (</a> of the current anchor element. */
void HtmlSpecialHandler::closeAnchor (SpecialActions &actions) {
if (_anchorType == AnchorType::HREF) {
markLinkedBox(actions);
actions.popContextElement();
_depthThreshold = 0;
}
_anchorType = AnchorType::NONE;
}


/** Marks a single rectangular area of the linked part of the document with a line or
* a box so that it's noticeable by the user. Additionally, an invisible rectangle is
* placed over this area in order to avoid flickering of the mouse cursor when moving
* it over the hyperlinked area. */
void HtmlSpecialHandler::markLinkedBox (SpecialActions &actions) {
const BoundingBox &bbox = actions.bbox("{anchor}");
if (bbox.width() > 0 && bbox.height() > 0) { // does the bounding box extend in both dimensions?
if (MARKER_TYPE != MarkerType::NONE) {
const double linewidth = min(0.5, bbox.height()/15);
XMLElementNode *rect = new XMLElementNode("rect");
double x = bbox.minX();
double y = bbox.maxY()+linewidth;
double w = bbox.width();
double h = linewidth;
const Color &linecolor = USE_LINECOLOR ? LINK_LINECOLOR : actions.getColor();
if (MARKER_TYPE == MarkerType::LINE)
rect->addAttribute("fill", linecolor.svgColorString());
else {
x -= linewidth;
y = bbox.minY()-linewidth;
w += 2*linewidth;
h += bbox.height()+linewidth;
if (MARKER_TYPE == MarkerType::BGCOLOR) {
rect->addAttribute("fill", LINK_BGCOLOR.svgColorString());
if (USE_LINECOLOR) {
rect->addAttribute("stroke", linecolor.svgColorString());
rect->addAttribute("stroke-width", linewidth);
}
}
else { // LM_BOX
rect->addAttribute("fill", "none");
rect->addAttribute("stroke", linecolor.svgColorString());
rect->addAttribute("stroke-width", linewidth);
}
}
rect->addAttribute("x", x);
rect->addAttribute("y", y);
rect->addAttribute("width", w);
rect->addAttribute("height", h);
actions.prependToPage(rect);
if (MARKER_TYPE == MarkerType::BOX || MARKER_TYPE == MarkerType::BGCOLOR) {
// slightly enlarge the boxed area
x -= linewidth;
y -= linewidth;
w += 2*linewidth;
h += 2*linewidth;
}
actions.embed(BoundingBox(x, y, x+w, y+h));
}
// Create an invisible rectangle around the linked area so that it's easier to access.
// This is only necessary when using paths rather than real text elements together with fonts.
if (!SVGTree::USE_FONTS) {
XMLElementNode *rect = new XMLElementNode("rect");
rect->addAttribute("x", bbox.minX());
rect->addAttribute("y", bbox.minY());
rect->addAttribute("width", bbox.width());
rect->addAttribute("height", bbox.height());
rect->addAttribute("fill", "white");
rect->addAttribute("fill-opacity", 0);
actions.appendToPage(rect);
}
}
}


/** This method is called every time the DVI position changes. */
void HtmlSpecialHandler::dviMovedTo (double x, double y, SpecialActions &actions) {
if (_active && _anchorType != AnchorType::NONE) {
// Start a new box if the current depth of the DVI stack underruns
// the initial threshold which indicates a line break.
if (actions.getDVIStackDepth() < _depthThreshold) {
markLinkedBox(actions);
_depthThreshold = actions.getDVIStackDepth();
actions.bbox("{anchor}", true); // start a new box on the new line
}
}
if (_active)
HyperlinkManager::instance().checkNewLine(actions);
}


void HtmlSpecialHandler::dviEndPage (unsigned pageno, SpecialActions &actions) {
if (_active) {
// create views for all collected named anchors defined on the recent page
const BoundingBox &pagebox = actions.bbox();
for (auto &stranchorpair : _namedAnchors) {
if (stranchorpair.second.pageno == pageno && stranchorpair.second.referenced) { // current anchor referenced?
ostringstream oss;
oss << pagebox.minX() << ' ' << stranchorpair.second.pos << ' '
<< pagebox.width() << ' ' << pagebox.height();
XMLElementNode *view = new XMLElementNode("view");
view->addAttribute("id", "loc"+XMLString(stranchorpair.second.id));
view->addAttribute("viewBox", oss.str());
actions.appendToDefs(view);
}
}
closeAnchor(actions);
HyperlinkManager::instance().createViews(pageno, actions);
_active = false;
}
}


/** Sets the appearance of the link markers.
* @param[in] marker string specifying the marker (format: type[:linecolor])
* @return true on success */
bool HtmlSpecialHandler::setLinkMarker (const string &marker) {
string type; // "none", "box", "line", or a background color specifier
string color; // optional line color specifier
size_t seppos = marker.find(":");
if (seppos == string::npos)
type = marker;
else {
type = marker.substr(0, seppos);
color = marker.substr(seppos+1);
}
if (type.empty() || type == "none")
MARKER_TYPE = MarkerType::NONE;
else if (type == "line")
MARKER_TYPE = MarkerType::LINE;
else if (type == "box")
MARKER_TYPE = MarkerType::BOX;
else {
if (!LINK_BGCOLOR.setPSName(type, false))
return false;
MARKER_TYPE = MarkerType::BGCOLOR;
}
USE_LINECOLOR = false;
if (MARKER_TYPE != MarkerType::NONE && !color.empty()) {
if (!LINK_LINECOLOR.setPSName(color, false))
return false;
USE_LINECOLOR = true;
}
return true;
}


const vector<const char*> HtmlSpecialHandler::prefixes () const {
const vector<const char*> pfx {"html:"};
return pfx;
Expand Down
35 changes: 2 additions & 33 deletions src/HtmlSpecialHandler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,52 +28,21 @@

class SpecialActions;

class HtmlSpecialHandler : public SpecialHandler, public DVIEndPageListener, public DVIPositionListener
{
struct NamedAnchor {
NamedAnchor () : pageno(0), id(0), pos(0), referenced(false) {}
NamedAnchor (unsigned pn, int i, double p, bool r=false) : pageno(pn), id(i), pos(p), referenced(r) {}
unsigned pageno; ///< page number where the anchor is located
int id; ///< unique numerical ID (< 0 if anchor is undefined yet)
double pos; ///< vertical position of named anchor (in PS point units)
bool referenced; ///< true if a reference to this anchor exists
};

enum class AnchorType {NONE, HREF, NAME};
using NamedAnchors = std::unordered_map<std::string, NamedAnchor>;

class HtmlSpecialHandler : public SpecialHandler, public DVIEndPageListener, public DVIPositionListener {
public:
HtmlSpecialHandler () : _active(false), _anchorType(AnchorType::NONE), _depthThreshold(0) {}
HtmlSpecialHandler () : _active(false) {}
void preprocess (const char *prefix, std::istream &is, SpecialActions &actions) override;
bool process (const char *prefix, std::istream &is, SpecialActions &actions) override;
const char* name () const override {return "html";}
const char* info () const override {return "hyperref specials";}
const std::vector<const char*> prefixes () const override;

static bool setLinkMarker (const std::string &marker);

protected:
void preprocessHrefAnchor (const std::string &uri);
void preprocessNameAnchor (const std::string &name, SpecialActions &actions);
void processHrefAnchor (std::string uri, SpecialActions &actions);
void processNameAnchor (const std::string &name, SpecialActions &actions);
void dviEndPage (unsigned pageno, SpecialActions &actions) override;
void dviMovedTo (double x, double y, SpecialActions &actions) override;
void closeAnchor (SpecialActions &actions);
void markLinkedBox (SpecialActions &actions);

enum class MarkerType {NONE, LINE, BOX, BGCOLOR};
static MarkerType MARKER_TYPE; ///< selects how to mark linked areas
static Color LINK_BGCOLOR; ///< background color if linkmark type == LT_BGCOLOR
static Color LINK_LINECOLOR; ///< line color if linkmark type is LM_LINE or LM_BOX
static bool USE_LINECOLOR; ///< if true, LINK_LINECOLOR is applied

private:
bool _active;
AnchorType _anchorType; ///< type of active anchor
int _depthThreshold; ///< break anchor box if the DVI stack depth underruns this threshold
std::string _base; ///< base URL that is prepended to all relative targets
NamedAnchors _namedAnchors; ///< information about all named anchors processed
};

#endif
Loading

0 comments on commit 8c8cb81

Please sign in to comment.