Parsing Hymn Specifications
Parsing hymn specifications is a combination of elements already encountered plus a couple of new wrinkles.
The most common case is a simple specification for one hymn by tag:
<hymns>
<hymn name="Conditor alme siderum"/>
</hymns>
As we've seen in some other contexts, this can be a list filtered by use:
<hymns>
<hymn use="Roman" name="O lux beata Trinitas"/>
<hymn use="Sarum" name="Lucis Creator Optime"/>
</hymns>
There is one really odd curveball. For feats of apostles First Vespers, there is a hymn, Annue, Christe, which has a variable second verse depending on the apostle. This is represented by:
<hymns>
<hymn name="Annue, Christe" verse="true">
Last chosen of the Twelve,
His dignity we own,
Who by the Apostles lot
Received the vacant throne
We who Matthias praise
Entreat thee, Lord of Love,
That throned in light and bliss,
We too may reign above.
</hymn>
</hymns>
This makes for some slightly-more-intricate-than-usual logic.
HymnTag
The HymnTag can have various subsets of three attributes. Only name is mandatory, but it also cannot have more than two at once. We don't actually bother testing for the latter case: what will in fact happen at the next level up is that if the verse is set, no processing of the use attributes takes place, which effectively reduces the number to two in any case.
class HymnTag : public BreviaryTag
{
public:
HymnTag(): BreviaryTag(GetTagName()) { }
~HymnTag() override;
const std::string& getHymn() const { return getAttribute("name"); }
Use getUse() const { return m_use; }
bool isVerse() const { return m_verse; }
static std::string GetTagName() { return "hymn"; }
private:
int
allowedAttributeCount() const override
{
return 3;
}
std::span<std::string> getAllowedAttributes() const override;
bool validate(std::string_view inAttribute,
std::string_view inValue) const override;
void setValue(std::string_view inAttribute,
std::string_view inValue) override;
bool checkMandatoryAttributes() const override { return hasAttribute("name"); }
Use m_use = Use::General;
bool m_verse = false;
};
std::span<std::string> HymnTag::getAllowedAttributes() const
{
static std::array<std::string, 3> rval{ "use", "name", "verse" };
return rval;
}
Validation allows for "York" as well as Sarum, as some hymns are sometimes derived from the Use of York.
bool HymnTag::validate(std::string_view inAttribute,
std::string_view inValue) const
{
if (inAttribute == "use"sv)
return ((inValue == "Roman"sv) || (inValue == "Sarum"sv)
|| (inValue == "York"sv));
else if (inAttribute == "verse"sv)
{
return ((inValue == "false"sv) || (inValue == "true"sv));
}
else
return !inValue.empty();
}
void HymnTag::setValue(std::string_view inAttribute, std::string_view inValue)
{
if (inAttribute == "use"sv)
{
if (inValue == "Roman"sv)
m_use = Use::Roman;
else
m_use = Use::Anglican;
}
if (inAttribute == "verse"sv)
m_verse = (inValue == "true"sv);
}
HymnElement
The HymnElement stores the information in a straightforward manner:
class HymnElement : public TextElementBase
{
public:
explicit HymnElement(std::string_view inText);
~HymnElement() override;
const std::string &getStartTagName() const override { return m_tag.getName(); }
const std::string& getHymn() const { return m_tag.getHymn(); }
bool isVerse() const { return m_tag.isVerse(); }
const std::string& getVerse() const { return m_verse; }
Use getUse() const { return m_tag.getUse(); }
private:
HymnTag m_tag;
std::string m_verse;
};
The special logic for extracting the verse is straightforward. Attributes are delegated to the tag, so no special processing is required for them.
HymnElement::HymnElement(std::string_view inText):
TextElementBase(inText, HymnTag::GetTagName())
{
auto l = [&](const HymnTag &inVal, const int inAttributes) {
if (inVal.isVerse())
{
if (inVal.isClosed())
throw OfficeParseException(
getStartTagName() + " verse type element must not be closed"
+ std::string(inText.substr(0, getLength())));
std::string_view body(inText.substr(getLength()));
auto index = body.find('<');
if (index == std::string::npos)
{
throw OfficeParseException(getStartTagName()
+ " body with no end");
}
m_verse = std::string(body.substr(0, index));
body.remove_prefix(incrementLength(m_verse.length()));
incrementLength(checkEndTag(body));
}
else if (!inVal.isClosed())
throw OfficeParseException(
getStartTagName()
+ " element must be closed and have no element or text content",
inText);
};
TagSetter().set<HymnTag>(m_tag, l, inText.substr(0, getLength()));
}
HymnsElement
Most of the real complexity occurs at the level of the HymnsElement.
class HymnsElement : public MultiElementElement
{
public:
HymnsElement(const Use inUse, std::string_view inText);
~HymnsElement() override;
const std::string &getHymn() const { return m_hymn; }
const std::string &getHymnVerse() const { return m_hymnVerse; }
private:
const std::string &getStartTagName() const override
{
return m_tag.getName();
}
void processHymnList(const Use inUse, std::string_view inText);
void extractHymn(const Use inUse, std::string_view inText,
const int inIndex);
int extractHymnVerse(std::string_view inText);
NoAttributeTag m_tag;
std::string m_hymn;
std::string m_hymnVerse;
};
Even with delegation to several private methods, the constructor is on the long side:
HymnsElement::HymnsElement(const Use inUse, std::string_view inText):
MultiElementElement(inText, "hymns"), m_tag("hymns")
{
auto l = [&](const NoAttributeTag &inVal, const int inAttributes) {
if (m_tag.isClosed())
throw OfficeParseException("Cannot have a reference " + getStartTagName()
+ " element for hymns");
std::string_view rest(inText.substr(getLength()));
adjustBetweenTags(rest);
auto index = rest.find(getEndTag());
if (index == std::string_view::npos)
throw OfficeParseException("Could not find a close tag for "
+ getStartTagName());
std::string_view head = rest.substr(0, index);
if (head.find("verse") == std::string::npos)
{
incrementLength(head.length() + getEndTag().length());
processHymnList(inUse, head);
}
else // there should be one hymn element with an Annue Christe verse
{
rest.remove_prefix(incrementLength(extractHymnVerse(rest)));
adjustBetweenTags(rest);
if (!rest.starts_with(getEndTag()))
throw OfficeParseException(
getStartTagName()
+ " with unexpected element before closing tag: "
+ std::string(rest.substr(0, 10)));
incrementLength(getEndTag().length());
}
if (m_hymn.empty())
throw OfficeParseException("There must be at least one valid hymn for "
+ getStartTagName());
};
TagSetter().set<NoAttributeTag>(m_tag, l, inText.substr(0, getLength()));
}
The methods add in checks affecting the verse variant as well as standard formal checks:
void HymnsElement::extractHymn(const Use inUse, std::string_view inText,
const int inIndex)
{
auto l = [&](const HymnTag &inVal, const int inAttributes) {
if ((inUse == Use::General) || (inVal.getUse() == Use::General)
|| (inVal.getUse() == inUse))
m_hymn = inVal.getHymn();
};
HymnTag tag;
TagSetter().set<HymnTag>(tag, l, inText.substr(0, inIndex + 1));
}
void HymnsElement::processHymnList(const Use inUse, std::string_view inText)
{
auto index = inText.find("<" + HymnTag::GetTagName());
std::string_view rest = inText;
while ((index != std::string::npos) && m_hymn.empty())
{
rest = rest.substr(index);
auto index2 = rest.find('>');
if (index2 == std::string_view::npos)
throw OfficeParseException(BreviaryTag::ErrorTypes::BadlyFormedElement,
"Missing close paren for "
+ std::string(rest.substr(0, 20)));
extractHymn(inUse, rest, index2);
rest.remove_prefix(index2 + 1);
index = rest.find("<" + HymnTag::GetTagName());
}
}
int HymnsElement::extractHymnVerse(std::string_view inText)
{
HymnElement element(inText);
if (!element.isVerse())
{
throw OfficeParseException(
"There must be only one hymn element for Annue, Christe verse for "
+ getStartTagName());
}
m_hymn = element.getHymn();
m_hymnVerse = element.getVerse();
return element.getLength();
}
Comments
Post a Comment