text:
#include "generator.h"
#include "atom.h"
#include "tree.h"
#include "qdocdatabase.h"
#include "qdoctagfiles.h"
#include "qdocindexfiles.h"
#include <qdebug.h>

QT_BEGIN_NAMESPACE

static NodeMap emptyNodeMap_;
static NodeMultiMap emptyNodeMultiMap_;

/*! \class QDocDatabase
 */

QDocDatabase* QDocDatabase::qdocDB_ = NULL;

/*!
  Constructs the singleton qdoc database object.
  It constructs a singleton Tree object with this
  qdoc database pointer.
 */
QDocDatabase::QDocDatabase() : showInternal_(false)
{
    tree_ = new Tree(this);
}

/*!
  Destroys the qdoc database object. This requires deleting
  the tree of nodes, which deletes each node.
 */
QDocDatabase::~QDocDatabase()
{
    masterMap_.clear();
    delete tree_;
}

/*! \fn Tree* QDocDatabase::tree()
  Returns the pointer to the tree. This function is for compatibility
  with the current qdoc. It will be removed when the QDocDatabase class
  replaces the current structures.
 */

/*!
  Creates the singleton. Allows only one instance of the class
  to be created. Returns a pointer to the singleton.
*/
QDocDatabase* QDocDatabase::qdocDB()
{
   if (!qdocDB_)
      qdocDB_ = new QDocDatabase;
   return qdocDB_;
}

/*!
  Destroys the singleton.
 */
void QDocDatabase::destroyQdocDB()
{
    if (qdocDB_) {
        delete qdocDB_;
        qdocDB_ = 0;
    }
}

/*!
  \fn const DocNodeMap& QDocDatabase::groups() const
  Returns a const reference to the collection of all
  group nodes.
*/

/*!
  \fn const DocNodeMap& QDocDatabase::modules() const
  Returns a const reference to the collection of all
  module nodes.
*/

/*!
  \fn const DocNodeMap& QDocDatabase::qmlModules() const
  Returns a const reference to the collection of all
  QML module nodes.
*/

/*!
  Find the group node named \a name and return a pointer
  to it. If a matching node is not found, return 0.
 */
DocNode* QDocDatabase::getGroup(const QString& name)
{
    DocNodeMap::const_iterator i = groups_.find(name);
    if (i != groups_.end())
        return i.value();
    return 0;
}

/*!
  Find the group node named \a name and return a pointer
  to it. If a matching node is not found, add a new group
  node named \a name and return a pointer to that one.

  If a new group node is added, its parent is the tree root,
  and the new group node is marked \e{not seen}.
 */
DocNode* QDocDatabase::findGroup(const QString& name)
{
    DocNodeMap::const_iterator i = groups_.find(name);
    if (i != groups_.end())
        return i.value();
    DocNode* dn = new DocNode(tree_->root(), name, Node::Group, Node::OverviewPage);
    dn->markNotSeen();
    groups_.insert(name,dn);
    if (!masterMap_.contains(name,dn))
        masterMap_.insert(name,dn);
    return dn;
}

/*!
  Find the module node named \a name and return a pointer
  to it. If a matching node is not found, add a new module
  node named \a name and return a pointer to that one.

  If a new module node is added, its parent is the tree root,
  and the new module node is marked \e{not seen}.
 */
DocNode* QDocDatabase::findModule(const QString& name)
{
    DocNodeMap::const_iterator i = modules_.find(name);
    if (i != modules_.end())
        return i.value();
    DocNode* dn = new DocNode(tree_->root(), name, Node::Module, Node::OverviewPage);
    dn->markNotSeen();
    modules_.insert(name,dn);
    if (!masterMap_.contains(name,dn))
        masterMap_.insert(name,dn);
    return dn;
}

/*!
  Find the QML module node named \a name and return a pointer
  to it. If a matching node is not found, add a new QML module
  node named \a name and return a pointer to that one.

  If a new QML module node is added, its parent is the tree root,
  and the new QML module node is marked \e{not seen}.
 */
QmlModuleNode* QDocDatabase::findQmlModule(const QString& name)
{
    if (qmlModules_.contains(name))
        return static_cast<QmlModuleNode*>(qmlModules_.value(name));

    QmlModuleNode* qmn = new QmlModuleNode(tree_->root(), name);
    qmn->markNotSeen();
    qmn->setQmlModuleInfo(name);
    qmlModules_.insert(name, qmn);
    masterMap_.insert(name, qmn);
    return qmn;
}

/*!
  Looks up the group node named \a name in the collection
  of all group nodes. If a match is found, a pointer to the
  node is returned. Otherwise, a new group node named \a name
  is created and inserted into the collection, and the pointer
  to that node is returned. The group node is marked \e{seen}
  in either case.
 */
DocNode* QDocDatabase::addGroup(const QString& name)
{
    DocNode* group = findGroup(name);
    group->markSeen();
    return group;
}

/*!
  Looks up the module node named \a name in the collection
  of all module nodes. If a match is found, a pointer to the
  node is returned. Otherwise, a new module node named \a name
  is created and inserted into the collection, and the pointer
  to that node is returned. The module node is marked \e{seen}
  in either case.
 */
DocNode* QDocDatabase::addModule(const QString& name)
{
    DocNode* module = findModule(name);
    module->markSeen();
    return module;
}

/*!
  Looks up the QML module node named \a name in the collection
  of all QML module nodes. If a match is found, a pointer to the
  node is returned. Otherwise, a new QML module node named \a name
  is created and inserted into the collection, and the pointer
  to that node is returned. The QML module node is marked \e{seen}
  in either case.
 */
QmlModuleNode* QDocDatabase::addQmlModule(const QString& name)
{
    QStringList blankSplit = name.split(QLatin1Char(' '));
    QmlModuleNode* qmn = findQmlModule(blankSplit[0]);
    qmn->setQmlModuleInfo(name);
    qmn->markSeen();
    //masterMap_.insert(qmn->qmlModuleIdentifier(),qmn);
    return qmn;
}

/*!
  Looks up the group node named \a name in the collection
  of all group nodes. If a match is not found, a new group
  node named \a name is created and inserted into the collection.
  Then append \a node to the group's members list, and append the
  group node to the member list of the \a node. The parent of the
  \a node is not changed by this function. Returns a pointer to
  the group node.
 */
DocNode* QDocDatabase::addToGroup(const QString& name, Node* node)
{
    DocNode* dn = findGroup(name);
    dn->addMember(node);
    node->addMember(dn);
    return dn;
}

/*!
  Looks up the module node named \a name in the collection
  of all module nodes. If a match is not found, a new module
  node named \a name is created and inserted into the collection.
  Then append \a node to the module's members list. The parent of
  \a node is not changed by this function. Returns the module node.
 */
DocNode* QDocDatabase::addToModule(const QString& name, Node* node)
{
    DocNode* dn = findModule(name);
    dn->addMember(node);
    node->setModuleName(name);
    return dn;
}

/*!
  Looks up the QML module named \a name. If it isn't there,
  create it. Then append \a node to the QML module's member
  list. The parent of \a node is not changed by this function.
 */
void QDocDatabase::addToQmlModule(const QString& name, Node* node)
{
    QStringList qmid;
    QStringList dotSplit;
    QStringList blankSplit = name.split(QLatin1Char(' '));
    qmid.append(blankSplit[0]);
    if (blankSplit.size() > 1) {
        qmid.append(blankSplit[0] + blankSplit[1]);
        dotSplit = blankSplit[1].split(QLatin1Char('.'));
        qmid.append(blankSplit[0] + dotSplit[0]);
    }

    QmlModuleNode* qmn = findQmlModule(blankSplit[0]);
    qmn->addMember(node);
    node->setQmlModule(qmn);

    if (node->subType() == Node::QmlClass) {
        QmlClassNode* n = static_cast<QmlClassNode*>(node);
        for (int i=0; i<qmid.size(); ++i) {
            QString key = qmid[i] + "::" + node->name();
            if (!qmlTypeMap_.contains(key))
                qmlTypeMap_.insert(key,n);
            if (!masterMap_.contains(key))
                masterMap_.insert(key,node);
        }
        if (!masterMap_.contains(node->name(),node))
            masterMap_.insert(node->name(),node);
    }
}

/*!
  Looks up the QML type node identified by the Qml module id
  \a qmid and QML type \a name and returns a pointer to the
  QML type node. The key is \a qmid + "::" + \a name.

  If the QML module id is empty, it looks up the QML type by
  \a name only.
 */
QmlClassNode* QDocDatabase::findQmlType(const QString& qmid, const QString& name) const
{
    if (!qmid.isEmpty())
        return qmlTypeMap_.value(qmid + "::" + name);

    QStringList path(name);
    Node* n = tree_->findNodeByNameAndType(path, Node::Document, Node::QmlClass, 0, true);
    if (n) {
        if (n->subType() == Node::QmlClass)
            return static_cast<QmlClassNode*>(n);
        else if (n->subType() == Node::Collision) {
            NameCollisionNode* ncn;
            ncn = static_cast<NameCollisionNode*>(n);
            return static_cast<QmlClassNode*>(ncn->findAny(Node::Document,Node::QmlClass));
        }
    }
    return 0;
}

/*!
  Looks up the QML type node identified by the Qml module id
  constructed from the strings in the \a import record and the
  QML type \a name and returns a pointer to the QML type node.
  If a QML type node is not found, 0 is returned.
 */
QmlClassNode* QDocDatabase::findQmlType(const ImportRec& import, const QString& name) const
{
    if (!import.isEmpty()) {
        QStringList dotSplit;
        dotSplit = name.split(QLatin1Char('.'));
        QString qmName;
        if (import.importUri_.isEmpty())
            qmName = import.name_;
        else
            qmName = import.importUri_;
        for (int i=0; i<dotSplit.size(); ++i) {
            QString qualifiedName = qmName + "::" + dotSplit[i];
            QmlClassNode* qcn = qmlTypeMap_.value(qualifiedName);
            if (qcn)
                return qcn;
        }
    }
    return 0;
}

/*!
  For debugging only.
 */
void QDocDatabase::printModules() const
{
    DocNodeMap::const_iterator i = modules_.begin();
    while (i != modules_.end()) {
        qDebug() << "  " << i.key();
        ++i;
    }
}

/*!
  For debugging only.
 */
void QDocDatabase::printQmlModules() const
{
    DocNodeMap::const_iterator i = qmlModules_.begin();
    while (i != qmlModules_.end()) {
        qDebug() << "  " << i.key();
        ++i;
    }
}

/*!
  Traverses the database to construct useful data structures
  for use when outputting certain significant collections of
  things, C++ classes, QML types, "since" lists, and other
  stuff.
 */
void QDocDatabase::buildCollections()
{
    nonCompatClasses_.clear();
    mainClasses_.clear();
    compatClasses_.clear();
    obsoleteClasses_.clear();
    funcIndex_.clear();
    legaleseTexts_.clear();
    serviceClasses_.clear();
    qmlClasses_.clear();

    findAllClasses(treeRoot());
    findAllFunctions(treeRoot());
    findAllLegaleseTexts(treeRoot());
    findAllNamespaces(treeRoot());
    findAllSince(treeRoot());
    findAllObsoleteThings(treeRoot());
}

/*!
  Finds all the C++ class nodes and QML type nodes and
  sorts them into maps.
 */
void QDocDatabase::findAllClasses(const InnerNode* node)
{
    NodeList::const_iterator c = node->childNodes().constBegin();
    while (c != node->childNodes().constEnd()) {
        if ((*c)->access() != Node::Private && (!(*c)->isInternal() || showInternal_)) {
            if ((*c)->type() == Node::Class && !(*c)->doc().isEmpty()) {
                QString className = (*c)->name();
                if ((*c)->parent() &&
                        (*c)->parent()->type() == Node::Namespace &&
                        !(*c)->parent()->name().isEmpty())
                    className = (*c)->parent()->name()+"::"+className;

                if ((*c)->status() == Node::Compat) {
                    compatClasses_.insert(className, *c);
                }
                else {
                    nonCompatClasses_.insert(className, *c);
                    if ((*c)->status() == Node::Main)
                        mainClasses_.insert(className, *c);
                }

                QString serviceName = (static_cast<const ClassNode *>(*c))->serviceName();
                if (!serviceName.isEmpty()) {
                    serviceClasses_.insert(serviceName, *c);
                }
            }
            else if ((*c)->type() == Node::Document &&
                     (*c)->subType() == Node::QmlClass &&
                     !(*c)->doc().isEmpty()) {
                QString qmlTypeName = (*c)->name();
                if (qmlTypeName.startsWith(QLatin1String("QML:")))
                    qmlClasses_.insert(qmlTypeName.mid(4),*c);
                else
                    qmlClasses_.insert(qmlTypeName,*c);
            }
            else if ((*c)->isInnerNode()) {
                findAllClasses(static_cast<InnerNode*>(*c));
            }
        }
        ++c;
    }
}

/*!
  Finds all the function nodes
 */
void QDocDatabase::findAllFunctions(const InnerNode* node)
{
    NodeList::ConstIterator c = node->childNodes().constBegin();
    while (c != node->childNodes().constEnd()) {
        if ((*c)->access() != Node::Private) {
            if ((*c)->isInnerNode()) {
                findAllFunctions(static_cast<const InnerNode*>(*c));
            }
            else if ((*c)->type() == Node::Function) {
                const FunctionNode* func = static_cast<const FunctionNode*>(*c);
                if ((func->status() > Node::Obsolete) &&
                        !func->isInternal() &&
                        (func->metaness() != FunctionNode::Ctor) &&
                        (func->metaness() != FunctionNode::Dtor)) {
                    funcIndex_[(*c)->name()].insert((*c)->parent()->fullDocumentName(), *c);
                }
            }
        }
        ++c;
    }
}

/*!
  Finds all the nodes containing legalese text and puts them
  in a map.
 */
void QDocDatabase::findAllLegaleseTexts(const InnerNode* node)
{
    NodeList::ConstIterator c = node->childNodes().constBegin();
    while (c != node->childNodes().constEnd()) {
        if ((*c)->access() != Node::Private) {
            if (!(*c)->doc().legaleseText().isEmpty())
                legaleseTexts_.insertMulti((*c)->doc().legaleseText(), *c);
            if ((*c)->isInnerNode())
                findAllLegaleseTexts(static_cast<const InnerNode *>(*c));
        }
        ++c;
    }
}

/*!
  Finds all the namespace nodes and puts them in an index.
 */
void QDocDatabase::findAllNamespaces(const InnerNode* node)
{
    NodeList::ConstIterator c = node->childNodes().constBegin();
    while (c != node->childNodes().constEnd()) {
        if ((*c)->access() != Node::Private) {
            if ((*c)->isInnerNode()) {
                findAllNamespaces(static_cast<const InnerNode *>(*c));
                if ((*c)->type() == Node::Namespace) {
                    // Ensure that the namespace's name is not empty (the root
                    // namespace has no name).
                    if (!(*c)->name().isEmpty())
                        namespaceIndex_.insert((*c)->name(), *c);
                }
            }
        }
        ++c;
    }
}

/*!
  Finds all nodes with status = Obsolete and sorts them into
  maps. They can be C++ classes, QML types, or they can be
  functions, enum types, typedefs, methods, etc.
 */
void QDocDatabase::findAllObsoleteThings(const InnerNode* node)
{
    NodeList::const_iterator c = node->childNodes().constBegin();
    while (c != node->childNodes().constEnd()) {
        if ((*c)->access() != Node::Private) {
            QString name = (*c)->name();
            if ((*c)->status() == Node::Obsolete) {
                if ((*c)->type() == Node::Class) {
                    if ((*c)->parent() && (*c)->parent()->type() == Node::Namespace &&
                        !(*c)->parent()->name().isEmpty())
                        name = (*c)->parent()->name() + "::" + name;
                    obsoleteClasses_.insert(name, *c);
                }
                else if ((*c)->type() == Node::Document && (*c)->subType() == Node::QmlClass) {
                    if (name.startsWith(QLatin1String("QML:")))
                        name = name.mid(4);
                    name = (*c)->qmlModuleName() + "::" + name;
                    obsoleteQmlTypes_.insert(name,*c);
                }
            }
            else if ((*c)->type() == Node::Class) {
                InnerNode* n = static_cast<InnerNode*>(*c);
                bool inserted = false;
                NodeList::const_iterator p = n->childNodes().constBegin();
                while (p != n->childNodes().constEnd()) {
                    if ((*p)->access() != Node::Private) {
                        switch ((*p)->type()) {
                        case Node::Enum:
                        case Node::Typedef:
                        case Node::Function:
                        case Node::Property:
                        case Node::Variable:
                            if ((*p)->status() == Node::Obsolete) {
                                if ((*c)->parent() && (*c)->parent()->type() == Node::Namespace &&
                                    !(*c)->parent()->name().isEmpty())
                                    name = (*c)->parent()->name() + "::" + name;
                                classesWithObsoleteMembers_.insert(name, *c);
                                inserted = true;
                            }
                            break;
                        default:
                            break;
                        }
                    }
                    if (inserted)
                        break;
                    ++p;
                }
            }
            else if ((*c)->type() == Node::Document && (*c)->subType() == Node::QmlClass) {
                InnerNode* n = static_cast<InnerNode*>(*c);
                bool inserted = false;
                NodeList::const_iterator p = n->childNodes().constBegin();
                while (p != n->childNodes().constEnd()) {
                    if ((*p)->access() != Node::Private) {
                        switch ((*c)->type()) {
                        case Node::QmlProperty:
                        case Node::QmlSignal:
                        case Node::QmlSignalHandler:
                        case Node::QmlMethod:
                            if ((*c)->parent()) {
                                Node* parent = (*c)->parent();
                                if (parent->type() == Node::QmlPropertyGroup && parent->parent())
                                    parent = parent->parent();
                                if (parent && parent->subType() == Node::QmlClass && !parent->name().isEmpty())
                                    name = parent->name() + "::" + name;
                            }
                            qmlTypesWithObsoleteMembers_.insert(name,*c);
                            inserted = true;
                            break;
                        default:
                            break;
                        }
                    }
                    if (inserted)
                        break;
                    ++p;
                }
            }
            else if ((*c)->isInnerNode()) {
                findAllObsoleteThings(static_cast<InnerNode*>(*c));
            }
        }
        ++c;
    }
}

/*!
  Finds all the nodes where a \e{since} command appeared in the
  qdoc comment and sorts them into maps according to the kind of
  node.

  This function is used for generating the "New Classes... in x.y"
  section on the \e{What's New in Qt x.y} page.
 */
void QDocDatabase::findAllSince(const InnerNode* node)
{
    NodeList::const_iterator child = node->childNodes().constBegin();
    while (child != node->childNodes().constEnd()) {
        QString sinceString = (*child)->since();
        // Insert a new entry into each map for each new since string found.
        if (((*child)->access() != Node::Private) && !sinceString.isEmpty()) {
            NodeMultiMapMap::iterator nsmap = newSinceMaps_.find(sinceString);
            if (nsmap == newSinceMaps_.end())
                nsmap = newSinceMaps_.insert(sinceString,NodeMultiMap());

            NodeMapMap::iterator ncmap = newClassMaps_.find(sinceString);
            if (ncmap == newClassMaps_.end())
                ncmap = newClassMaps_.insert(sinceString,NodeMap());

            NodeMapMap::iterator nqcmap = newQmlTypeMaps_.find(sinceString);
            if (nqcmap == newQmlTypeMaps_.end())
                nqcmap = newQmlTypeMaps_.insert(sinceString,NodeMap());

            if ((*child)->type() == Node::Function) {
                // Insert functions into the general since map.
                FunctionNode *func = static_cast<FunctionNode *>(*child);
                if ((func->status() > Node::Obsolete) &&
                    (func->metaness() != FunctionNode::Ctor) &&
                    (func->metaness() != FunctionNode::Dtor)) {
                    nsmap.value().insert(func->name(),(*child));
                }
            }
            else {
                if ((*child)->type() == Node::Class) {
                    // Insert classes into the since and class maps.
                    QString className = (*child)->name();
                    if ((*child)->parent() && !(*child)->parent()->name().isEmpty()) {
                        className = (*child)->parent()->name()+"::"+className;
                    }
                    nsmap.value().insert(className,(*child));
                    ncmap.value().insert(className,(*child));
                }
                else if ((*child)->subType() == Node::QmlClass) {
                    // Insert QML elements into the since and element maps.
                    QString className = (*child)->name();
                    if ((*child)->parent() && !(*child)->parent()->name().isEmpty()) {
                        className = (*child)->parent()->name()+"::"+className;
                    }
                    nsmap.value().insert(className,(*child));
                    nqcmap.value().insert(className,(*child));
                }
                else if ((*child)->type() == Node::QmlProperty) {
                    // Insert QML properties into the since map.
                    QString propertyName = (*child)->name();
                    nsmap.value().insert(propertyName,(*child));
                }
                else {
                    // Insert external documents into the general since map.
                    QString name = (*child)->name();
                    if ((*child)->parent() && !(*child)->parent()->name().isEmpty()) {
                        name = (*child)->parent()->name()+"::"+name;
                    }
                    nsmap.value().insert(name,(*child));
                }
            }
            // Recursively find child nodes with since commands.
            if ((*child)->isInnerNode()) {
                findAllSince(static_cast<InnerNode *>(*child));
            }
        }
        ++child;
    }
}

/*!
  Find the \a key in the map of new class maps, and return a
  reference to the value, which is a NodeMap. If \a key is not
  found, return a reference to an empty NodeMap.
 */
const NodeMap& QDocDatabase::getClassMap(const QString& key) const
{
    NodeMapMap::const_iterator i = newClassMaps_.constFind(key);
    if (i != newClassMaps_.constEnd())
        return i.value();
    return emptyNodeMap_;
}

/*!
  Find the \a key in the map of new QML type maps, and return a
  reference to the value, which is a NodeMap. If the \a key is not
  found, return a reference to an empty NodeMap.
 */
const NodeMap& QDocDatabase::getQmlTypeMap(const QString& key) const
{
    NodeMapMap::const_iterator i = newQmlTypeMaps_.constFind(key);
    if (i != newQmlTypeMaps_.constEnd())
        return i.value();
    return emptyNodeMap_;
}

/*!
  Find the \a key in the map of new \e {since} maps, and return
  a reference to the value, which is a NodeMultiMap. If \a key
  is not found, return a reference to an empty NodeMultiMap.
 */
const NodeMultiMap& QDocDatabase::getSinceMap(const QString& key) const
{
    NodeMultiMapMap::const_iterator i = newSinceMaps_.constFind(key);
    if (i != newSinceMaps_.constEnd())
        return i.value();
    return emptyNodeMultiMap_;
}

/*!
  Performs several housekeeping algorithms that create
  certain data structures and resolve lots of links, prior
  to generating documentation.
 */
void QDocDatabase::resolveIssues() {
    resolveQmlInheritance(treeRoot());
    resolveTargets(treeRoot());
    tree_->resolveCppToQmlLinks();
}

/*!
  Searches the \a database for a node named \a target and returns
  a pointer to it if found.
 */
const Node* QDocDatabase::resolveTarget(const QString& target,
                                        const Node* relative,
                                        const Node* self)
{
    const Node* node = 0;
    if (target.endsWith("()")) {
        QString funcName = target;
        funcName.chop(2);
        QStringList path = funcName.split("::");
        const FunctionNode* fn = tree_->findFunctionNode(path, relative, SearchBaseClasses);
        if (fn) {
            /*
              Why is this case not accepted?
             */
            if (fn->metaness() != FunctionNode::MacroWithoutParams)
                node = fn;
        }
    }
    else if (target.contains(QLatin1Char('#'))) {
        // This error message is never printed; I think we can remove the case.
        qDebug() << "qdoc: target case not handled:" << target;
    }
    else {
        QStringList path = target.split("::");
        int flags = SearchBaseClasses | SearchEnumValues | NonFunction;
        node = tree_->findNode(path, relative, flags, self);
    }
    return node;
}

/*!
  Finds the node that will generate the documentation that
  contains the \a target and returns a pointer to it.
 */
const Node* QDocDatabase::findNodeForTarget(const QString& target, const Node* relative)
{
    const Node* node = 0;
    if (target.isEmpty())
        node = relative;
    else if (target.endsWith(".html"))
        node = tree_->root()->findChildNodeByNameAndType(target, Node::Document);
    else {
        node = resolveTarget(target, relative);
        if (!node)
            node = findDocNodeByTitle(target, relative);
    }
    return node;
}

/*!
  Inserts a new target into the target table with the specified
  \a name, \a node, and \a priority.
 */
void QDocDatabase::insertTarget(const QString& name, TargetRec::Type type, Node* node, int priority)
{
    TargetRec target;
    target.type_ = type;
    target.node_ = node;
    target.priority_ = priority;
    Atom a = Atom(Atom::Target, name);
    target.ref_ = refForAtom(&a);
    targetRecMultiMap_.insert(name, target);
}

/*!
  This function searches for a \a target anchor node. If it
  finds one, it sets \a ref and returns the found node.
 */
const Node*
QDocDatabase::findUnambiguousTarget(const QString& target, QString& ref, const Node* relative)
{
    TargetRec bestTarget;
    int numBestTargets = 0;
    QList<TargetRec> bestTargetList;

    QString key = Doc::canonicalTitle(target);
    TargetRecMultiMap::iterator i = targetRecMultiMap_.find(key);
    while (i != targetRecMultiMap_.end()) {
        if (i.key() != key)
            break;
        const TargetRec& candidate = i.value();
        if (candidate.priority_ < bestTarget.priority_) {
            bestTarget = candidate;
            bestTargetList.clear();
            bestTargetList.append(candidate);
            numBestTargets = 1;
        } else if (candidate.priority_ == bestTarget.priority_) {
            bestTargetList.append(candidate);
            ++numBestTargets;
        }
        ++i;
    }
    if (numBestTargets > 0) {
        if (numBestTargets == 1) {
            ref = bestTarget.ref_;
            return bestTarget.node_;
        }
        else if (bestTargetList.size() > 1) {
            if (relative && !relative->qmlModuleName().isEmpty()) {
                for (int i=0; i<bestTargetList.size(); ++i) {
                    const Node* n = bestTargetList.at(i).node_;
                    if (n && relative->qmlModuleName() == n->qmlModuleName()) {
                        ref = bestTargetList.at(i).ref_;
                        return n;
                    }
                }
            }
        }
    }
    ref.clear();
    return 0;
}

/*!
  This function searches for a node with the specified \a title.
  If \a relative node is provided, it is used to disambiguate if
  it has a QML module identifier.
 */
const DocNode* QDocDatabase::findDocNodeByTitle(const QString& title, const Node* relative) const
{
    QString key = Doc::canonicalTitle(title);
    DocNodeMultiMap::const_iterator i = docNodesByTitle_.constFind(key);
    if (i != docNodesByTitle_.constEnd()) {
        if (relative && !relative->qmlModuleName().isEmpty()) {
            const DocNode* dn = i.value();
            InnerNode* parent = dn->parent();
            if (parent && parent->type() == Node::Document && parent->subType() == Node::Collision) {
                const NodeList& nl = parent->childNodes();
                NodeList::ConstIterator it = nl.constBegin();
                while (it != nl.constEnd()) {
                    if ((*it)->qmlModuleName() == relative->qmlModuleName()) {
                        /*
                          By returning here, we avoid printing
                          all the duplicate header warnings,
                          which are not really duplicates now,
                          because of the QML module name being
                          used as a namespace qualifier.
                        */
                        dn = static_cast<const DocNode*>(*it);
                        return dn;
                    }
                    ++it;
                }
            }
        }
        /*
          Reporting all these duplicate section titles is probably
          overkill. We should report the duplicate file and let
          that suffice.
        */
        DocNodeMultiMap::const_iterator j = i;
        ++j;
        if (j != docNodesByTitle_.constEnd() && j.key() == i.key()) {
            QList<Location> internalLocations;
            while (j != docNodesByTitle_.constEnd()) {
                if (j.key() == i.key() && j.value()->url().isEmpty()) {
                    internalLocations.append(j.value()->location());
                    break; // Just report one duplicate for now.
                }
                ++j;
            }
            if (internalLocations.size() > 0) {
                i.value()->location().warning(tr("This page title exists in more than one file: \"%1\"").arg(title));
                foreach (const Location &location, internalLocations)
                    location.warning(tr("[It also exists here]"));
            }
        }
        return i.value();
    }
    return 0;
}

/*!
  This function searches for a node with a canonical title
  constructed from \a target. If the node it finds is \a node,
  it returns the ref from that node. Otherwise it returns an
  empty string.
 */
QString QDocDatabase::findTarget(const QString& target, const Node* node) const
{
    QString key = Doc::canonicalTitle(target);
    TargetRecMultiMap::const_iterator i = targetRecMultiMap_.constFind(key);

    if (i != targetRecMultiMap_.constEnd()) {
        do {
            if (i.value().node_ == node)
                return i.value().ref_;
            ++i;
        } while (i != targetRecMultiMap_.constEnd() && i.key() == key);
    }
    return QString();
}

/*!
  For each QML Type node in the tree beginning at \a root,
  if it has a QML base type name but its QML base type node
  pointer is 0, use the QML base type name to look up the
  base type node. If the node is found in the tree, set the
  node's QML base type node pointer.
 */
void QDocDatabase::resolveQmlInheritance(InnerNode* root)
{
    // Dop we need recursion?
    foreach (Node* child, root->childNodes()) {
        if (child->type() == Node::Document && child->subType() == Node::QmlClass) {
            QmlClassNode* qcn = static_cast<QmlClassNode*>(child);
            if ((qcn->qmlBaseNode() == 0) && !qcn->qmlBaseName().isEmpty()) {
                QmlClassNode* bqcn = 0;
                if (qcn->qmlBaseName().contains("::")) {
                    bqcn =  qmlTypeMap_.value(qcn->qmlBaseName());
                }
                else {
                    const ImportList& imports = qcn->importList();
                    for (int i=0; i<imports.size(); ++i) {
                        bqcn = findQmlType(imports[i], qcn->qmlBaseName());
                        if (bqcn)
                            break;
                    }
                }
                if (bqcn == 0) {
                    bqcn = findQmlType(QString(), qcn->qmlBaseName());
                }
                if (bqcn) {
                    qcn->setQmlBaseNode(bqcn);
                }
#if 0
                else {
                    qDebug() << "Temporary error message (ignore): UNABLE to resolve QML base type:"
                             << qcn->qmlBaseName() << "for QML type:" << qcn->name();
                }
#endif
            }
        }
    }
}

/*!
 */
void QDocDatabase::resolveTargets(InnerNode* root)
{
    // need recursion

    foreach (Node* child, root->childNodes()) {
        if (child->type() == Node::Document) {
            DocNode* node = static_cast<DocNode*>(child);
            if (!node->title().isEmpty()) {
                QString key = Doc::canonicalTitle(node->title());
                QList<DocNode*> nodes = docNodesByTitle_.values(key);
                bool alreadyThere = false;
                if (!nodes.empty()) {
                    for (int i=0; i< nodes.size(); ++i) {
                        if (nodes[i]->subType() == Node::ExternalPage) {
                            if (node->name() == nodes[i]->name()) {
                                alreadyThere = true;
                                break;
                            }
                        }
                    }
                }
                if (!alreadyThere) {
                    docNodesByTitle_.insert(key, node);
                }
            }
            if (node->subType() == Node::Collision) {
                resolveTargets(node);
            }
        }

        if (child->doc().hasTableOfContents()) {
            const QList<Atom*>& toc = child->doc().tableOfContents();
            TargetRec target;
            target.node_ = child;
            target.priority_ = 3;

            for (int i = 0; i < toc.size(); ++i) {
                target.ref_ = refForAtom(toc.at(i));
                QString title = Text::sectionHeading(toc.at(i)).toString();
                if (!title.isEmpty()) {
                    QString key = Doc::canonicalTitle(title);
                    targetRecMultiMap_.insert(key, target);
                }
            }
        }
        if (child->doc().hasKeywords()) {
            const QList<Atom*>& keywords = child->doc().keywords();
            TargetRec target;
            target.node_ = child;
            target.priority_ = 1;

            for (int i = 0; i < keywords.size(); ++i) {
                target.ref_ = refForAtom(keywords.at(i));
                QString key = Doc::canonicalTitle(keywords.at(i)->string());
                targetRecMultiMap_.insert(key, target);
            }
        }
        if (child->doc().hasTargets()) {
            const QList<Atom*>& toc = child->doc().targets();
            TargetRec target;
            target.node_ = child;
            target.priority_ = 2;

            for (int i = 0; i < toc.size(); ++i) {
                target.ref_ = refForAtom(toc.at(i));
                QString key = Doc::canonicalTitle(toc.at(i)->string());
                targetRecMultiMap_.insert(key, target);
            }
        }
    }
}

/*!
  Generates a tag file and writes it to \a name.
 */
void QDocDatabase::generateTagFile(const QString& name, Generator* g)
{
    if (!name.isEmpty()) {
        QDocTagFiles::qdocTagFiles()->generateTagFile(name, g);
        QDocTagFiles::destroyQDocTagFiles();
    }
}

/*!
  Reads and parses the qdoc index files listed in \a indexFiles.
 */
void QDocDatabase::readIndexes(const QStringList& indexFiles)
{
    QDocIndexFiles::qdocIndexFiles()->readIndexes(indexFiles);
    QDocIndexFiles::destroyQDocIndexFiles();
}

/*!
  Generates a qdoc index file and write it to \a fileName. The
  index file is generated with the parameters \a url, \a title,
  \a g, and \a generateInternalNodes.
 */
void QDocDatabase::generateIndex(const QString& fileName,
                                 const QString& url,
                                 const QString& title,
                                 Generator* g,
                                 bool generateInternalNodes)
{
    QDocIndexFiles::qdocIndexFiles()->generateIndex(fileName, url, title, g, generateInternalNodes);
    QDocIndexFiles::destroyQDocIndexFiles();
}

QString QDocDatabase::refForAtom(const Atom* atom)
{
    if (atom) {
        if (atom->type() == Atom::SectionLeft)
            return Doc::canonicalTitle(Text::sectionHeading(atom).toString());
        if (atom->type() == Atom::Target)
            return Doc::canonicalTitle(atom->string());
    }
    return QString();
}

/*!
  If there are open namespaces, search for the function node
  having the same function name as the \a clone node in each
  open namespace. The \a parentPath is a portion of the path
  name provided with the function name at the point of
  reference. \a parentPath is usually a class name. Return
  the pointer to the function node if one is found in an
  open namespace. Otherwise return 0.

  This open namespace concept is of dubious value and might
  be removed.
 */
FunctionNode* QDocDatabase::findNodeInOpenNamespace(const QStringList& parentPath,
                                                    const FunctionNode* clone)
{
    FunctionNode* fn = 0;
    if (!openNamespaces_.isEmpty()) {
        foreach (const QString& t, openNamespaces_) {
            QStringList path = t.split("::") + parentPath;
            fn = findFunctionNode(path, clone);
            if (fn)
                break;
        }
    }
    return fn;
}

/*!
  Find a node of the specified \a type and \a subtype that is
  reached with the specified \a path. If such a node is found
  in an open namespace, prefix \a path with the name of the
  open namespace and "::" and return a pointer to the node.
  Othewrwise return 0.
 */
Node* QDocDatabase::findNodeInOpenNamespace(QStringList& path,
                                            Node::Type type,
                                            Node::SubType subtype)
{
    Node* n = 0;
    if (!openNamespaces_.isEmpty()) {
        foreach (const QString& t, openNamespaces_) {
            QStringList p = t.split("::") + path;
            n = findNodeByNameAndType(p, type, subtype);
            if (n) {
                path = p;
                break;
            }
        }
    }
    return n;
}

QT_END_NAMESPACE


text:
<?xml version="1.0" encoding="utf-8"?>
<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor
	license agreements. See the NOTICE file distributed with this work for additional
	information regarding copyright ownership. The ASF licenses this file to
	you under the Apache License, Version 2.0 (the "License"); you may not use
	this file except in compliance with the License. You may obtain a copy of
	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
	by applicable law or agreed to in writing, software distributed under the
	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
	OF ANY KIND, either express or implied. See the License for the specific
	language governing permissions and limitations under the License. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.sap.espm</groupId>
	<artifactId>espm</artifactId>
	<version>0.2.1</version>
	<packaging>pom</packaging>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

		<!-- Properties that hold the local server information required for local
			integration testing -->
		<local.server.root>${project.build.directory}/server</local.server.root>
		<local.server.host>localhost</local.server.host>

		<!-- Property that holds the source path to the backend destinations -->
		<destinations.path>${basedir}/../destinations</destinations.path>

		<!-- Properties needed to reference sap OData -->
		<sap.odata.version>0.7.0</sap.odata.version>
		<sap.odata.processor.version>0.7.0</sap.odata.processor.version>
		<!-- use 'com.sap.odata' when testing released version 0.6.0 -->
		<sap.odata.groupId>com.sap.odata</sap.odata.groupId>

		<m2e.jaxrs.activation>false</m2e.jaxrs.activation>
	</properties>

	<modules>
		<module>espm-cloud-jpa</module>
		<module>espm-cloud-web</module>
		<module>espm-mobile-shopping-web</module>
		<module>espm-ui-reviews-jpa</module>
		<module>espm-ui-reviews-web</module>
	</modules>

	<dependencies>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.2</version>
		</dependency>
		<!-- Test dependencies -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.11</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-library</artifactId>
			<version>1.2.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.seleniumhq.selenium</groupId>
			<artifactId>selenium-java</artifactId>
			<version>2.37.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.7</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.derby</groupId>
			<artifactId>derby</artifactId>
			<version>10.9.1.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.7.2</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.1</version>
					<configuration>
						<source>1.6</source>
						<target>1.6</target>
					</configuration>
				</plugin>
				<plugin>
					<artifactId>maven-war-plugin</artifactId>
					<version>2.3</version> <!-- prevent Maven warning -->
				</plugin>
				<!--This plugin's configuration is used to store Eclipse m2e settings
					only. It has no influence on the Maven build itself. -->
				<plugin>
					<groupId>org.eclipse.m2e</groupId>
					<artifactId>lifecycle-mapping</artifactId>
					<version>1.0.0</version>
					<configuration>
						<lifecycleMappingMetadata>
							<pluginExecutions>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>
											org.apache.maven.plugins
										</groupId>
										<artifactId>
											maven-dependency-plugin
										</artifactId>
										<versionRange>[2.4,)</versionRange>
										<goals>
											<goal>copy-dependencies</goal>
											<goal>unpack-dependencies</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<execute>
											<runOnIncremental>false</runOnIncremental>
										</execute>
									</action>
								</pluginExecution>
							</pluginExecutions>
						</lifecycleMappingMetadata>
					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>

	<profiles>
		<!-- Choose correct command line scripts for interaction with SAP HANA
			Cloud based on detected operating system -->
		<profile>
			<id>unix-scripts</id>
			<activation>
				<os>
					<family>Unix</family>
				</os>
			</activation>
			<properties>
				<sap.cloud.console.shell>sh</sap.cloud.console.shell>
				<sap.cloud.console.script>neo.sh</sap.cloud.console.script>
			</properties>
		</profile>
		<profile>
			<id>windows-scripts</id>
			<activation>
				<os>
					<family>Windows</family>
				</os>
			</activation>
			<properties>
				<sap.cloud.console.shell>cmd</sap.cloud.console.shell>
				<sap.cloud.console.script>/c neo.bat</sap.cloud.console.script>
			</properties>
		</profile>
	</profiles>

</project>

redpajama_set_name: RedPajamaGithub


text:
using System;
using Xunit;
namespace Humanizer.Tests.Localisation.de
{
    [UseCulture("de-DE")]
    public class TimeSpanHumanizeTests
    {
        [Theory]
        [InlineData(7, "Eine Woche")]
        [InlineData(14, "2 Wochen")]
        [InlineData(21, "3 Wochen")]
        [InlineData(77, "11 Wochen")]
        public void Weeks(int days, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromDays(days).Humanize());
        }

        [Theory]
        [InlineData(1, "Ein Tag")]
        [InlineData(2, "2 Tage")]
        public void Days(int days, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromDays(days).Humanize());
        }

        [Theory]
        [InlineData(1, "Eine Stunde")]
        [InlineData(2, "2 Stunden")]
        public void Hours(int hours, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromHours(hours).Humanize());
        }

        [Theory]
        [InlineData(1, "Eine Minute")]
        [InlineData(2, "2 Minuten")]
        public void Minutes(int minutes, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromMinutes(minutes).Humanize());
        }

        [Theory]
        [InlineData(1, "Eine Sekunde")]
        [InlineData(2, "2 Sekunden")]
        public void Seconds(int seconds, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromSeconds(seconds).Humanize());
        }

        [Theory]
        [InlineData(1, "Eine Millisekunde")]
        [InlineData(2, "2 Millisekunden")]
        public void Milliseconds(int milliseconds, string expected)
        {
            Assert.Equal(expected, TimeSpan.FromMilliseconds(milliseconds).Humanize());
        }

        [Fact]
        public void NoTime()
        {
            // This one doesn't make a lot of sense but ... w/e
            Assert.Equal("Keine Zeit", TimeSpan.Zero.Humanize());
        }
    }
}


text:
import {
  ANALYTICS_TAG_NAME,
  StoryAnalyticsEvent,
} from '../../amp-story/1.0/story-analytics';
import {clamp} from '#core/math';
import {
  Action,
  StateProperty,
} from '../../amp-story/1.0/amp-story-store-service';
import {AnalyticsVariable} from '../../amp-story/1.0/variable-service';
import {CSS} from '../../../build/amp-story-interactive-0.1.css';
import {Services} from '#service';
import {
  addParamsToUrl,
  appendPathToUrl,
  assertAbsoluteHttpOrHttpsUrl,
} from '../../../src/url';
import {base64UrlEncodeFromString} from '#core/types/string/base64';
import {assertDoesNotContainDisplay, setImportantStyles} from '#core/dom/style';
import {
  buildInteractiveDisclaimer,
  buildInteractiveDisclaimerIcon,
} from './interactive-disclaimer';
import {closest} from '#core/dom/query';
import {
  createShadowRootWithStyle,
  maybeMakeProxyUrl,
} from '../../amp-story/1.0/utils';
import {deduplicateInteractiveIds} from './utils';
import {dev, devAssert} from '../../../src/log';
import {dict} from '#core/types/object';
import {emojiConfetti} from './interactive-confetti';
import {toArray} from '#core/types/array';
import {isExperimentOn} from '#experiments/';

/** @const {string} */
const TAG = 'amp-story-interactive';

/** @const {string} */
export const MID_SELECTION_CLASS = 'i-amphtml-story-interactive-mid-selection';
/** @const {string} */
export const POST_SELECTION_CLASS =
  'i-amphtml-story-interactive-post-selection';

/**
 * @const @enum {number}
 */
export const InteractiveType = {
  QUIZ: 0,
  POLL: 1,
  RESULTS: 2,
  SLIDER: 3,
};

/** @const {string} */
const ENDPOINT_INVALID_ERROR =
  'The publisher has specified an invalid datastore endpoint';

/** @const {string} */
const INTERACTIVE_ACTIVE_CLASS = 'i-amphtml-story-interactive-active';

/**
 * @typedef {{
 *    index: number,
 *    count: number,
 *    selected: boolean,
 * }}
 */
export let InteractiveOptionType;

/**
 * @typedef {{
 *    options: !Array<InteractiveOptionType>,
 * }}
 */
export let InteractiveResponseType;

/**
 * @typedef {{
 *    optionIndex: number,
 *    text: string,
 *    correct: ?string,
 *    resultscategory: ?string,
 *    image: ?string,
 *    confetti: ?string,
 *    resultsthreshold: ?string,
 * }}
 */
export let OptionConfigType;

/** @const {Array<Object>} fontFaces with urls from https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap */
const fontsToLoad = [
  {
    family: 'Poppins',
    weight: '400',
    src: "url(https://fonts.gstatic.com/s/poppins/v9/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format('woff2')",
  },
  {
    family: 'Poppins',
    weight: '700',
    src: "url(https://fonts.gstatic.com/s/poppins/v9/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) format('woff2')",
  },
];

/**
 * Interactive abstract class with shared functionality for interactive components.
 *
 * Lifecycle:
 * 1) When created, the abstract class will call the buildComponent() method implemented by each concrete class.
 *   NOTE: When created, the component will receive a .i-amphtml-story-interactive-component, inheriting useful CSS variables.
 *
 * 2) If an endpoint is specified, it will retrieve aggregate results from the backend and process them. If the clientId
 *   has responded in a previous session, the component will change to a post-selection state. Otherwise it will wait
 *   for user selection.
 *   NOTE: Click listeners will be attached to all options, which require .i-amphtml-story-interactive-option.
 *
 * 3) On user selection, it will process the backend results (if endpoint specified) and display the selected option.
 *   Analytic events will be sent, percentages updated (implemented by the concrete class), and backend posted with the
 *   user response. Classes will be added to the component and options accordingly.
 *   NOTE: On option selected, the selection will receive a .i-amphtml-story-interactive-option-selected, and the root element
 *   will receive a .i-amphtml-story-interactive-post-selection. Optionally, if the endpoint returned aggregate results,
 *   the root element will also receive a .i-amphtml-story-interactive-has-data.
 *
 * @abstract
 */
export class AmpStoryInteractive extends AMP.BaseElement {
  /**
   * @param {!AmpElement} element
   * @param {!InteractiveType} type
   * @param {!Array<number>} bounds the bounds on number of options, inclusive
   */
  constructor(element, type, bounds = [2, 4]) {
    super(element);

    /** @protected @const {InteractiveType} */
    this.interactiveType_ = type;

    /** @protected {?../../amp-story/1.0/story-analytics.StoryAnalyticsService} */
    this.analyticsService_ = null;

    /** @protected {?Promise<?InteractiveResponseType|?JsonObject|undefined>} */
    this.backendDataPromise_ = null;

    /** @protected {?Promise<JsonObject>} */
    this.clientIdPromise_ = null;

    /** @private {?Element} the disclaimer dialog if open, null if closed */
    this.disclaimerEl_ = null;

    /** @private {?Element} */
    this.disclaimerIcon_ = null;

    /** @protected {boolean} */
    this.hasUserSelection_ = false;

    /** @private {!Array<number>} min and max number of options, inclusive */
    this.optionBounds_ = bounds;

    /** @private {?Array<!Element>} DOM elements that have the i-amphtml-story-interactive-option class */
    this.optionElements_ = null;

    /** @protected {?Array<!OptionConfigType>} option config values from attributes (text, correct...) */
    this.options_ = null;

    /** @protected {?Array<!InteractiveOptionType>} retrieved results from the backend */
    this.optionsData_ = null;

    /** @private {?Element} the page element the component is on */
    this.pageEl_ = null;

    /** @protected {?Element} */
    this.rootEl_ = null;

    /** @public {../../../src/service/localizationService} */
    this.localizationService = null;

    /** @protected {?../../amp-story/1.0/amp-story-request-service.AmpStoryRequestService} */
    this.requestService_ = null;

    /** @protected {?../../amp-story/1.0/amp-story-store-service.AmpStoryStoreService} */
    this.storeService_ = null;

    /** @protected {?../../../src/service/url-impl.Url} */
    this.urlService_ = null;

    /** @protected {?../../amp-story/1.0/variable-service.AmpStoryVariableService} */
    this.variableService_ = null;
  }

  /**
   * Gets the root element.
   * @visibleForTesting
   * @return {?Element}
   */
  getRootElement() {
    return this.rootEl_;
  }

  /**
   * Gets the options.
   * @protected
   * @return {!Array<!Element>}
   */
  getOptionElements() {
    if (!this.optionElements_) {
      this.optionElements_ = toArray(
        this.rootEl_.querySelectorAll('.i-amphtml-story-interactive-option')
      );
    }
    return this.optionElements_;
  }

  /**
   * Gets the interactive ID
   * @private
   * @return {string}
   */
  getInteractiveId_() {
    if (!AmpStoryInteractive.canonicalUrl64) {
      deduplicateInteractiveIds(this.win.document);
      AmpStoryInteractive.canonicalUrl64 = base64UrlEncodeFromString(
        Services.documentInfoForDoc(this.element).canonicalUrl
      );
    }
    return `${AmpStoryInteractive.canonicalUrl64}+${this.element.id}`;
  }

  /**
   * @protected
   * @return {Element} the page element
   */
  getPageEl() {
    if (this.pageEl_ == null) {
      this.pageEl_ = closest(dev().assertElement(this.element), (el) => {
        return el.tagName.toLowerCase() === 'amp-story-page';
      });
    }
    return this.pageEl_;
  }

  /** @override */
  buildCallback(concreteCSS = '') {
    this.loadFonts_();
    this.options_ = this.parseOptions_();
    this.element.classList.add('i-amphtml-story-interactive-component');
    this.adjustGridLayer_();
    devAssert(this.element.children.length == 0, 'Too many children');

    // Initialize all the services before proceeding, and update store with state
    this.urlService_ = Services.urlForDoc(this.element);
    return Promise.all([
      Services.storyVariableServiceForOrNull(this.win).then((service) => {
        this.variableService_ = service;
      }),
      Services.storyStoreServiceForOrNull(this.win).then((service) => {
        this.storeService_ = service;
        this.updateStoryStoreState_(null);
      }),
      Services.storyRequestServiceForOrNull(this.win).then((service) => {
        this.requestService_ = service;
      }),
      Services.storyAnalyticsServiceForOrNull(this.win).then((service) => {
        this.analyticsService_ = service;
      }),
      Services.localizationServiceForOrNull(this.element).then((service) => {
        this.localizationService = service;
      }),
    ]).then(() => {
      this.rootEl_ = this.buildComponent();
      this.rootEl_.classList.add('i-amphtml-story-interactive-container');
      if (
        isExperimentOn(this.win, 'amp-story-interactive-disclaimer') &&
        this.element.hasAttribute('endpoint')
      ) {
        this.disclaimerIcon_ = buildInteractiveDisclaimerIcon(this);
        this.rootEl_.prepend(this.disclaimerIcon_);
      }
      createShadowRootWithStyle(
        this.element,
        dev().assertElement(this.rootEl_),
        CSS + concreteCSS
      );
      return Promise.resolve();
    });
  }

  /**
   * @private
   */
  loadFonts_() {
    if (
      !AmpStoryInteractive.loadedFonts &&
      this.win.document.fonts &&
      FontFace
    ) {
      fontsToLoad.forEach((fontProperties) => {
        const font = new FontFace(fontProperties.family, fontProperties.src, {
          weight: fontProperties.weight,
          style: 'normal',
        });
        font.load().then(() => {
          this.win.document.fonts.add(font);
        });
      });
    }
    AmpStoryInteractive.loadedFonts = true;
  }

  /**
   * Reads the element attributes prefixed with option- and returns them as a list.
   * eg: [
   *      {optionIndex: 0, text: 'Koala'},
   *      {optionIndex: 1, text: 'Developers', correct: ''}
   *    ]
   * @protected
   * @return {?Array<!OptionConfigType>}
   */
  parseOptions_() {
    const options = [];
    toArray(this.element.attributes).forEach((attr) => {
      // Match 'option-#-type' (eg: option-1-text, option-2-image, option-3-correct...)
      if (attr.name.match(/^option-\d+(-\w+)+$/)) {
        const splitParts = attr.name.split('-');
        const optionNumber = parseInt(splitParts[1], 10);
        // Add all options in order on the array with correct index.
        while (options.length < optionNumber) {
          options.push({'optionIndex': options.length});
        }
        const key = splitParts.slice(2).join('');
        if (key === 'image') {
          options[optionNumber - 1][key] = maybeMakeProxyUrl(
            attr.value,
            this.getAmpDoc()
          );
        } else {
          options[optionNumber - 1][key] = attr.value;
        }
      }
    });
    if (
      options.length >= this.optionBounds_[0] &&
      options.length <= this.optionBounds_[1]
    ) {
      return options;
    }
    devAssert(
      options.length >= this.optionBounds_[0] &&
        options.length <= this.optionBounds_[1],
      `Improper number of options. Expected ${this.optionBounds_[0]} <= options <= ${this.optionBounds_[1]} but got ${options.length}.`
    );
    dev().error(
      TAG,
      `Improper number of options. Expected ${this.optionBounds_[0]} <= options <= ${this.optionBounds_[1]} but got ${options.length}.`
    );
  }

  /**
   * Finds the prompt and adds it to the prompt-container
   *
   * @protected
   * @param {Element} root
   */
  attachPrompt_(root) {
    const promptContainer = root.querySelector(
      '.i-amphtml-story-interactive-prompt-container'
    );

    if (!this.element.hasAttribute('prompt-text')) {
      this.rootEl_.removeChild(promptContainer);
    } else {
      const prompt = document.createElement('p');
      prompt.textContent = this.element.getAttribute('prompt-text');
      prompt.classList.add('i-amphtml-story-interactive-prompt');
      promptContainer.appendChild(prompt);
    }
  }

  /**
   * Generates the template from the config_ Map.
   *
   * @return {!Element} rootEl_
   * @protected @abstract
   */
  buildComponent() {
    // Subclass must override.
  }

  /** @override */
  layoutCallback() {
    this.initializeListeners_();
    return (this.backendDataPromise_ = this.element.hasAttribute('endpoint')
      ? this.retrieveInteractiveData_()
      : Promise.resolve());
  }

  /**
   * Gets a Promise to return the unique AMP clientId
   * @private
   * @return {Promise<string>}
   */
  getClientId_() {
    if (!this.clientIdPromise_) {
      this.clientIdPromise_ = Services.cidForDoc(this.element).then((data) => {
        return data.get(
          {scope: 'amp-story', createCookieIfNotPresent: true},
          /* consent */ Promise.resolve()
        );
      });
    }
    return this.clientIdPromise_;
  }

  /**
   * Reacts to RTL state updates and triggers the UI for RTL.
   * @param {boolean} rtlState
   * @private
   */
  onRtlStateUpdate_(rtlState) {
    this.mutateElement(() => {
      rtlState
        ? this.rootEl_.setAttribute('dir', 'rtl')
        : this.rootEl_.removeAttribute('dir');
    });
  }

  /** @override */
  isLayoutSupported(layout) {
    return layout === 'container';
  }

  /**
   * Add classes to adjust the bottom padding on the grid-layer
   * to prevent overlap with the component.
   *
   * @private
   */
  adjustGridLayer_() {
    const gridLayer = closest(dev().assertElement(this.element), (el) => {
      return el.tagName.toLowerCase() === 'amp-story-grid-layer';
    });

    gridLayer.classList.add('i-amphtml-story-has-interactive');

    if (gridLayer.parentElement.querySelector('amp-story-cta-layer')) {
      gridLayer.classList.add('i-amphtml-story-has-CTA-layer');
    }

    if (gridLayer.parentElement.querySelector('amp-story-page-attachment')) {
      gridLayer.classList.add('i-amphtml-story-has-page-attachment');
    }
  }

  /**
   * Attaches functions to each option to handle state transition.
   * @private
   */
  initializeListeners_() {
    this.storeService_.subscribe(
      StateProperty.RTL_STATE,
      (rtlState) => {
        this.onRtlStateUpdate_(rtlState);
      },
      true /** callToInitialize */
    );

    // Check if the component page is active, and add class.
    this.storeService_.subscribe(
      StateProperty.CURRENT_PAGE_ID,
      (currPageId) => {
        this.mutateElement(() => {
          const toggle = currPageId === this.getPageEl().getAttribute('id');
          this.rootEl_.classList.toggle(INTERACTIVE_ACTIVE_CLASS, toggle);
          this.toggleTabbableElements_(toggle);
        });
        this.closeDisclaimer_();
      },
      true /** callToInitialize */
    );

    this.rootEl_.addEventListener('click', (e) => this.handleTap_(e));
  }

  /**
   * Handles a tap event on the quiz element.
   * @param {Event} e
   * @protected
   */
  handleTap_(e) {
    if (e.target == this.disclaimerIcon_ && !this.disclaimerEl_) {
      this.openDisclaimer_();
      return;
    }

    if (this.hasUserSelection_) {
      return;
    }

    const optionEl = closest(
      dev().assertElement(e.target),
      (element) => {
        return element.classList.contains('i-amphtml-story-interactive-option');
      },
      this.rootEl_
    );

    if (optionEl) {
      this.updateStoryStoreState_(optionEl.optionIndex_);
      this.handleOptionSelection_(optionEl.optionIndex_, optionEl);
      const confettiEmoji = this.options_[optionEl.optionIndex_].confetti;
      if (confettiEmoji) {
        emojiConfetti(
          dev().assertElement(this.rootEl_),
          this.win,
          confettiEmoji
        );
      }
      this.closeDisclaimer_();
    }
  }

  /**
   * Triggers the analytics event for quiz response.
   *
   * @param {number} optionIndex
   * @private
   */
  triggerAnalytics_(optionIndex) {
    this.variableService_.onVariableUpdate(
      AnalyticsVariable.STORY_INTERACTIVE_ID,
      this.element.getAttribute('id')
    );
    this.variableService_.onVariableUpdate(
      AnalyticsVariable.STORY_INTERACTIVE_RESPONSE,
      optionIndex
    );
    this.variableService_.onVariableUpdate(
      AnalyticsVariable.STORY_INTERACTIVE_TYPE,
      this.interactiveType_
    );

    this.element[ANALYTICS_TAG_NAME] = this.element.tagName;
    this.analyticsService_.triggerEvent(
      StoryAnalyticsEvent.INTERACTIVE,
      this.element
    );
  }

  /**
   * Update component to reflect values in the data obtained.
   * Called when user has responded (in this session or before).
   *
   * @protected @abstract
   * @param {!Array<!InteractiveOptionType>} unusedOptionsData
   */
  displayOptionsData(unusedOptionsData) {
    // Subclass must implement
  }

  /**
   * Preprocess the percentages for display.
   *
   * @param {!Array<!InteractiveOptionType>} optionsData
   * @return {Array<number>}
   * @protected
   */
  preprocessPercentages_(optionsData) {
    const totalResponseCount = optionsData.reduce(
      (acc, response) => acc + response['count'],
      0
    );

    let percentages = optionsData.map((e) =>
      ((100 * e['count']) / totalResponseCount).toFixed(2)
    );
    let total = percentages.reduce((acc, x) => acc + Math.round(x), 0);

    // Special case: divide remainders by three if they break 100,
    // 3 is the maximum above 100 the remainders can add.
    if (total > 100) {
      percentages = percentages.map((percentage) =>
        (percentage - (2 * (percentage - Math.floor(percentage))) / 3).toFixed(
          2
        )
      );
      total = percentages.reduce((acc, x) => (acc += Math.round(x)), 0);
    }

    if (total === 100) {
      return percentages.map((percentage) => Math.round(percentage));
    }

    // Truncate all and round up those with the highest remainders,
    // preserving order and ties and adding to 100 (if possible given ties and ordering).
    let remainder = 100 - total;

    let preserveOriginal = percentages.map((percentage, index) => {
      return {
        originalIndex: index,
        value: percentage,
        remainder: (percentage - Math.floor(percentage)).toFixed(2),
      };
    });
    preserveOriginal.sort(
      (left, right) =>
        // Break remainder ties using the higher value.
        right.remainder - left.remainder || right.value - left.value
    );

    const finalPercentages = [];
    while (remainder > 0 && preserveOriginal.length !== 0) {
      const highestRemainderObj = preserveOriginal[0];

      const ties = preserveOriginal.filter(
        (percentageObj) => percentageObj.value === highestRemainderObj.value
      );
      preserveOriginal = preserveOriginal.filter(
        (percentageObj) => percentageObj.value !== highestRemainderObj.value
      );

      const toRoundUp =
        ties.length <= remainder && highestRemainderObj.remainder !== '0.00';

      ties.forEach((percentageObj) => {
        finalPercentages[percentageObj.originalIndex] =
          Math.floor(percentageObj.value) + (toRoundUp ? 1 : 0);
      });

      // Update the remainder given additions to the percentages.
      remainder -= toRoundUp ? ties.length : 0;
    }

    preserveOriginal.forEach((percentageObj) => {
      finalPercentages[percentageObj.originalIndex] = Math.floor(
        percentageObj.value
      );
    });

    return finalPercentages;
  }

  /**
   * Triggers changes to component state on response interactive.
   *
   * @param {number} optionIndex
   * @param {?Element} optionEl
   * @private
   */
  handleOptionSelection_(optionIndex, optionEl) {
    this.backendDataPromise_
      .then(() => {
        if (this.hasUserSelection_) {
          return;
        }

        this.triggerAnalytics_(optionIndex);
        this.hasUserSelection_ = true;

        if (this.optionsData_) {
          this.optionsData_[optionIndex]['count']++;
          this.optionsData_[optionIndex]['selected'] = true;
        }

        this.mutateElement(() => {
          this.updateToPostSelectionState_(optionEl);
        });

        if (this.element.hasAttribute('endpoint')) {
          this.executeInteractiveRequest_('POST', optionIndex);
        }
      })
      .catch(() => {
        // If backend is not properly connected, still update state.
        this.triggerAnalytics_(optionIndex);
        this.hasUserSelection_ = true;
        this.mutateElement(() => {
          this.updateToPostSelectionState_(optionEl);
        });
      });
  }

  /**
   * Get the Interactive data from the datastore
   *
   * @return {?Promise<?InteractiveResponseType|?JsonObject|undefined>}
   * @private
   */
  retrieveInteractiveData_() {
    return this.executeInteractiveRequest_('GET').then((response) => {
      this.onDataRetrieved_(/** @type {InteractiveResponseType} */ (response));
    });
  }

  /**
   * Executes a Interactive API call.
   *
   * @param {string} method GET or POST.
   * @param {number=} optionSelected
   * @return {!Promise<!InteractiveResponseType|string>}
   * @private
   */
  executeInteractiveRequest_(method, optionSelected = undefined) {
    let url = this.element.getAttribute('endpoint');
    if (!assertAbsoluteHttpOrHttpsUrl(url)) {
      return Promise.reject(ENDPOINT_INVALID_ERROR);
    }

    return this.getClientId_().then((clientId) => {
      const requestOptions = {'method': method};
      const requestParams = dict({
        'type': this.interactiveType_,
        'client': clientId,
      });
      url = appendPathToUrl(
        this.urlService_.parse(url),
        this.getInteractiveId_()
      );
      if (requestOptions['method'] === 'POST') {
        requestOptions['body'] = {'option_selected': optionSelected};
        requestOptions['headers'] = {'Content-Type': 'application/json'};
        url = appendPathToUrl(this.urlService_.parse(url), ':vote');
      }
      url = addParamsToUrl(url, requestParams);
      return this.requestService_
        .executeRequest(url, requestOptions)
        .catch((err) => dev().error(TAG, err));
    });
  }

  /**
   * Handles incoming interactive data response
   *
   * RESPONSE FORMAT
   * {
   *  options: [
   *    {
   *      index:
   *      count:
   *      selected:
   *    },
   *    ...
   *  ]
   * }
   * @param {InteractiveResponseType|undefined} response
   * @private
   */
  onDataRetrieved_(response) {
    if (!(response && response['options'])) {
      devAssert(
        response && 'options' in response,
        `Invalid interactive response, expected { data: InteractiveResponseType, ...} but received ${response}`
      );
      dev().error(
        TAG,
        `Invalid interactive response, expected { data: InteractiveResponseType, ...} but received ${response}`
      );
      return;
    }
    const numOptions = this.getNumberOfOptions();
    // Only keep the visible options to ensure visible percentages add up to 100.
    this.updateComponentWithData(response['options'].slice(0, numOptions));
  }

  /**
   * Updates the quiz to reflect the state of the remote data.
   * @param {!Array<InteractiveOptionType>} data
   * @protected
   */
  updateComponentWithData(data) {
    const options = this.rootEl_.querySelectorAll(
      '.i-amphtml-story-interactive-option'
    );

    this.optionsData_ = this.orderData_(data);
    this.optionsData_.forEach((response) => {
      if (response.selected) {
        this.hasUserSelection_ = true;
        this.updateStoryStoreState_(response.index);
        this.mutateElement(() => {
          this.updateToPostSelectionState_(options[response.index]);
        });
      }
    });
  }

  /**
   * Updates the selected classes on component and option selected.
   * @param {?Element} selectedOption
   * @protected
   */
  updateToPostSelectionState_(selectedOption) {
    this.rootEl_.classList.add(POST_SELECTION_CLASS);
    if (selectedOption != null) {
      selectedOption.classList.add(
        'i-amphtml-story-interactive-option-selected'
      );
    }

    if (this.optionsData_) {
      this.rootEl_.classList.add('i-amphtml-story-interactive-has-data');
      this.displayOptionsData(this.optionsData_);
    }
    this.getOptionElements().forEach((el) => {
      el.setAttribute('tabindex', -1);
    });
  }

  /**
   * @public
   * @param {?number} option
   */
  updateStoryStoreState_(option = null) {
    const update = {
      option: option != null ? this.options_[option] : null,
      interactiveId: this.getInteractiveId_(),
      type: this.interactiveType_,
    };
    this.storeService_.dispatch(Action.ADD_INTERACTIVE_REACT, update);
  }

  /**
   * Toggles the tabbable elements (buttons, links, etc) to only reach them when page is active.
   * @param {boolean} toggle
   */
  toggleTabbableElements_(toggle) {
    this.rootEl_.querySelectorAll('button, a').forEach((el) => {
      // Disable tabbing through options if already selected.
      if (
        el.classList.contains('i-amphtml-story-interactive-option') &&
        this.hasUserSelection_
      ) {
        el.setAttribute('tabindex', -1);
      } else {
        el.setAttribute('tabindex', toggle ? 0 : -1);
      }
    });
  }

  /**
   * Returns the number of options.
   *
   * @protected
   * @return {number}
   */
  getNumberOfOptions() {
    return this.getOptionElements().length;
  }

  /**
   * Reorders options data to account for scrambled or incomplete data.
   *
   * @private
   * @param {!Array<!InteractiveOptionType>} optionsData
   * @return {!Array<!InteractiveOptionType>}
   */
  orderData_(optionsData) {
    const numOptionElements = this.getNumberOfOptions();
    const orderedData = new Array(numOptionElements);
    optionsData.forEach((option) => {
      const {index} = option;
      if (index >= 0 && index < numOptionElements) {
        orderedData[index] = option;
      }
    });

    for (let i = 0; i < orderedData.length; i++) {
      if (!orderedData[i]) {
        orderedData[i] = {
          count: 0,
          index: i,
          selected: false,
        };
      }
    }

    return orderedData;
  }

  /**
   * Opens the disclaimer dialog and positions it according to the page and itself.
   * @private
   */
  openDisclaimer_() {
    if (this.disclaimerEl_) {
      return;
    }
    const dir = this.rootEl_.getAttribute('dir') || 'ltr';
    this.disclaimerEl_ = buildInteractiveDisclaimer(this, {dir});

    let styles;
    this.measureMutateElement(
      () => {
        // Get rects and calculate position from icon.
        const interactiveRect = this.element./*OK*/ getBoundingClientRect();
        const pageRect = this.getPageEl()./*OK*/ getBoundingClientRect();
        const iconRect = this.disclaimerIcon_./*OK*/ getBoundingClientRect();
        const bottomFraction =
          1 - (iconRect.y + iconRect.height - pageRect.y) / pageRect.height;
        const widthFraction = interactiveRect.width / pageRect.width;

        // Clamp values to ensure dialog has space up and left.
        const bottomPercentage = clamp(bottomFraction * 100, 0, 85); // Ensure 15% of space up.
        const widthPercentage = Math.max(widthFraction * 100, 65); // Ensure 65% of max-width.

        styles = {
          'bottom': bottomPercentage + '%',
          'max-width': widthPercentage + '%',
          'position': 'absolute',
          'z-index': 3,
        };

        // Align disclaimer to left if RTL, otherwise align to the right.
        if (dir === 'rtl') {
          const leftFraction = (iconRect.x - pageRect.x) / pageRect.width;
          styles['left'] = clamp(leftFraction * 100, 0, 25) + '%'; // Ensure 75% of space to the right.
        } else {
          const rightFraction =
            1 - (iconRect.x + iconRect.width - pageRect.x) / pageRect.width;
          styles['right'] = clamp(rightFraction * 100, 0, 25) + '%'; // Ensure 75% of space to the left.
        }
      },
      () => {
        setImportantStyles(
          this.disclaimerEl_,
          assertDoesNotContainDisplay(styles)
        );
        this.getPageEl().appendChild(this.disclaimerEl_);
        this.disclaimerIcon_.setAttribute('hide', '');
        // Add click listener through the shadow dom using e.path.
        this.disclaimerEl_.addEventListener('click', (e) => {
          if (
            e.path[0].classList.contains(
              'i-amphtml-story-interactive-disclaimer-close'
            )
          ) {
            this.closeDisclaimer_();
          }
        });
      }
    );
  }

  /**
   * Closes the disclaimer dialog if open.
   * @private
   */
  closeDisclaimer_() {
    if (!this.disclaimerEl_) {
      return;
    }
    this.mutateElement(() => {
      this.disclaimerEl_.remove();
      this.disclaimerEl_ = null;
      if (this.disclaimerIcon_) {
        this.disclaimerIcon_.removeAttribute('hide');
      }
    });
  }
}


text:
'use strict';

function KineticEngine (context, utils) {
    context.scrollLeft = 0;
    context.scrollTop = 0;
    context.lastScrollLeft = 0;
    context.lastScrollTop = 0;
    context.targetTop = 0;
    context.targetLeft = 0;

    context.velocityTop = 0;
    context.velocityLeft = 0;
    context.amplitudeTop = 0;
    context.amplitudeLeft = 0;

    context.timeStamp = 0;
    context.referenceX = 0;
    context.referenceY = 0;
    context.pressed = false;
    context.autoScrollTracker = null;
    context.isAutoScrolling = false;

    context.leftTracker = function () {
        var now, elapsed, delta;

        now = utils.getTime();
        elapsed = now - context.timeStamp;
        context.timeStamp = now;
        delta = context.scrollLeft - context.lastScrollLeft;
        context.lastScrollLeft = context.scrollLeft;

        context.velocityLeft = context.userOptions.movingAverage * (1000 * delta / (1 + elapsed)) + 0.2 * context.velocityLeft;
    }

    context.topTracker = function () {
        var now, elapsed, delta;

        now = utils.getTime();
        elapsed = now - context.timeStamp;
        context.timeStamp = now;
        delta = context.scrollTop - context.lastScrollTop;
        context.lastScrollTop = context.scrollTop;

        context.velocityTop = context.userOptions.movingAverage * (1000 * delta / (1 + elapsed)) + 0.2 * context.velocityTop;
    }

    context.scroll = function(left, top) {
        var correctedLeft = Math.round(left);
        var correctedTop = Math.round(top);

        context.childNodes.forEach(function(node) {
            var $el = node.children[0];
            var maxScrollX = $el.scrollWidth - $el.clientWidth;
            var maxScrollY = $el.scrollHeight - $el.clientHeight;

            if ( maxScrollX > 0 && correctedLeft >= 0 && correctedLeft <= maxScrollX ) {
                $el.scrollLeft = correctedLeft;
                context.scrollLeft = correctedLeft;
            }
            if ( maxScrollY > 0 && correctedTop >= 0 && correctedTop <= maxScrollY ) {
                $el.scrollTop = correctedTop;
                context.scrollTop = correctedTop;
            }
        })
    }

    context.autoScroll = function() {
        var elapsed;
        var deltaY = 0, deltaX = 0, scrollX = 0, scrollY = 0;
        var timeConstant = 325;

        elapsed = utils.getTime() - context.timeStamp;

        if ( context.amplitudeTop ) {
            deltaY = -context.amplitudeTop * Math.exp(-elapsed / timeConstant);
        }
        if ( context.amplitudeLeft ) {
            deltaX = -context.amplitudeLeft * Math.exp(-elapsed / timeConstant);
        }

        if ( deltaX > 0.5 || deltaX < -0.5 ) {
            scrollX = deltaX;
        } else {
            scrollX = 0;
        }

        if ( deltaY > 0.5 || deltaY < -0.5 ) {
            scrollY = deltaY;
        } else {
            scrollY = 0;
        }

        context.scroll(context.targetLeft + scrollX, context.targetTop + scrollY);

        if ( scrollX !== 0 || scrollY !== 0 ) {
            context.autoScrollTracker = requestAnimationFrame(context.autoScroll);
        } else {
            context.isAutoScrolling = false;
            context.autoScrollTracker = null;
        }
    }

    context.triggerAutoScroll = function (targetLeft, targetTop, amplitudeLeft, amplitudeTop) {
        if ( amplitudeLeft !== 0 || amplitudeTop !== 0 ) {
            context.cancelAutoScroll();

            context.timeStamp = utils.getTime();
            context.targetLeft = targetLeft;
            context.targetTop = targetTop;
            context.amplitudeLeft = amplitudeLeft;
            context.amplitudeTop = amplitudeTop;

            context.isAutoScrolling = true;
            context.autoScrollTracker = requestAnimationFrame(context.autoScroll);
        }
    }

    context.cancelAutoScroll = function () {
        if ( context.isAutoScrolling ) {
            cancelAnimationFrame(context.autoScrollTracker);
            context.isAutoScrolling = false;
            context.autoScrollTracker = null;
        }
    }

    context.tap = function (e) {
        context.pressed = true;
        context.referenceX = utils.getPoint(e, context.hasTouch).x;
        context.referenceY = utils.getPoint(e, context.hasTouch).y;

        context.velocityTop = context.amplitudeTop = 0;
        context.velocityLeft = context.amplitudeLeft = 0;

        context.lastScrollTop = context.scrollTop;
        context.lastScrollLeft = context.scrollLeft;

        context.timeStamp = utils.getTime();

        context.cancelAutoScroll();

        context.$listener.addEventListener( 'mousemove', context.swipe, true );
        context.$listener.addEventListener( 'mouseup', context.release, true );

        if ( utils.preventDefaultException(e.target, context.userOptions.preventDefaultException) ) {
            e.preventDefault();
        }
    }

    context.swipe = function (e) {
        var x, y, deltaX, deltaY;

        if (context.pressed) {
            x = utils.getPoint(e, context.hasTouch).x;
            y = utils.getPoint(e, context.hasTouch).y;

            deltaX = context.referenceX - x;
            deltaY = context.referenceY - y;

            if (deltaX > 2 || deltaX < -2) {
                context.referenceX = x;
            } else {
                deltaX = 0;
            }
            if (deltaY > 2 || deltaY < -2) {
                context.referenceY = y;
            } else {
                deltaY = 0;
            }

            context.topTracker();
            context.leftTracker();

            context.scroll( context.scrollLeft + deltaX, context.scrollTop + deltaY );
        }
    }

    context.release = function() {
        var targetLeft, targetTop, amplitudeLeft, amplitudeTop;

        context.pressed = false;

        context.topTracker();
        context.leftTracker();

        if (context.velocityLeft > 10 || context.velocityLeft < -10) {
            amplitudeLeft = 0.8 * context.velocityLeft;
            targetLeft = Math.round(context.scrollLeft + amplitudeLeft);
        } else {
            targetLeft = context.scrollLeft;
        }
        if (context.velocityTop > 10 || context.velocityTop < -10) {
            amplitudeTop = 0.8 * context.velocityTop;
            targetTop = Math.round(context.scrollTop + amplitudeTop);
        } else {
            targetTop = context.scrollTop;
        }

        context.triggerAutoScroll(targetLeft, targetTop, amplitudeLeft, amplitudeTop);

        context.$listener.removeEventListener( 'mousemove', context.swipe );
        context.$listener.removeEventListener( 'mouseup', context.release );
    }

    if ( ! context.hasTouch && context.userOptions.enableKinetics ) {
        context.$listener.addEventListener( 'mousedown', context.tap, true );
    }

    context.$on('$destroy', function() {
        context.$listener.removeEventListener( 'mousedown', context.tap );
    });

    var scrollGen = function (start, left, top) {
        return function () {
            var targetLeft = 0,
                targetTop = 0,
                amplitudeLeft = 0,
                amplitudeTop = 0,
                maxScroll = {};

            if ( start ) {
                targetLeft = left ? 0 : context.scrollLeft;
                targetTop = top ? 0 : context.scrollTop;
                amplitudeLeft = left ? -context.scrollLeft : 0;
                amplitudeTop = top ? -context.scrollTop : 0;
            } else {
                maxScroll = utils.getMaxScroll(context.childNodes);

                targetLeft = left ? maxScroll.left : context.scrollLeft;
                targetTop = top ? maxScroll.top : context.scrollTop;
                amplitudeLeft = left ? maxScroll.left - context.scrollLeft : 0;
                amplitudeTop = top ? maxScroll.top - context.scrollTop : 0;
            }

            context.triggerAutoScroll(targetLeft, targetTop, amplitudeLeft, amplitudeTop);
        }
    }

    var scrollToBy = function (addTo) {
        return function (left, top) {
            var maxScroll, numLeft, corrLeft, numTop, corrTop, targetLeft, targetTop, moveLeft, moveTop, amplitudeLeft, amplitudeTop;

            maxScroll = utils.getMaxScroll(context.childNodes);

            numLeft = parseInt(left);
            numTop = parseInt(top);

            corrLeft = isNaN( numLeft ) ? context.scrollLeft : (addTo ? numLeft + context.scrollLeft : numLeft);
            corrTop = isNaN( numTop ) ? context.scrollTop : (addTo ? numTop + context.scrollTop : numTop);

            targetLeft = corrLeft > maxScroll.left ? maxScroll.left : (corrLeft < 0 ? 0 : corrLeft);
            targetTop = corrTop > maxScroll.top ? maxScroll.top : (corrTop < 0 ? 0 : corrTop);

            moveLeft = context.scrollLeft - targetLeft !== 0 ? true : false;
            moveTop = context.scrollTop - targetTop !== 0 ? true : false;

            amplitudeLeft = moveLeft ? targetLeft - context.scrollLeft : 0;
            amplitudeTop = moveTop ? targetTop - context.scrollTop : 0;

            context.triggerAutoScroll(targetLeft, targetTop, amplitudeLeft, amplitudeTop);
        }
    }

    var start = true,
        notStart = false,
        left = true,
        notLeft = false,
        top = true,
        notTop = false,
        toValue = false,
        byValue = true;

    context.exposedMethods = {
        scrollToStart: scrollGen(start, left, top),
        scrollToStartLeft: scrollGen(start, left, notTop),
        scrollToStartTop: scrollGen(start, notLeft, top),
        scrollToEnd: scrollGen(notStart, left, top),
        scrollToEndLeft: scrollGen(notStart, left, notTop),
        scrollToEndTop: scrollGen(notStart, notLeft, top),
        scrollToPosition: scrollToBy(toValue),
        scrollByValue: scrollToBy(byValue)
    }
}

module.exports = KineticEngine;


text:
module Munchie::Parsers
  #
  class ScalarFraction < Munchie::Parser
    def pattern
      [:scalar, :scalar_fraction]
    end

    def parse(tokens, index)
      scalar   = tokens[index].get_tag(:scalar)
      fraction = tokens[index+1].get_tag(:scalar_fraction)

      new_val = scalar.type.to_f + fraction.value.to_f

      frac = Token.new(new_val)
      frac.tag(Munchie::Tokens::Scalar.new(frac, new_val))

      tokens.delete_at(index+1)
      tokens.delete_at(index)

      tokens.insert(index, frac)
    end
  end
end


text:
<!DOCTYPE html>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Version Migration — Flask AppBuilder</title>

    <link rel="stylesheet" href="_static/basic.css" type="text/css" />
    <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
    <link rel="stylesheet" href="_static/bootswatch-3.3.4/flatly/bootstrap.min.css" type="text/css" />
    <link rel="stylesheet" href="_static/bootstrap-sphinx.css" type="text/css" />

    <link rel="top" title="Flask AppBuilder" href="index.html" />
    <link rel="next" title="Versions" href="versions.html" />
    <link rel="prev" title="API Reference" href="api.html" />
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1'>
<meta name="apple-mobile-web-app-capable" content="yes">

        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">

        </button>

          Flask-AppBuilder
        1.4.6

Diagrams

  Site

Introduction

Installation

Command Line Manager

Base Configuration

Base Views

Model Views (Quick How to)

Model Views on MongoDB

Chart Views

Model Views with Files and Images

Quick Minimal Application

Model Relations

Actions

Advanced Configuration

Customizing

Templates

Generic Data Sources

Multiple Databases

i18n Translations

Security

User Registration

API Reference

Version Migration

Versions

  Page

Version Migration

Migrating from 1.2.X to 1.3.X

Migrating from 1.1.X to 1.2.X

Migrating from 0.9.X to 0.10.X

Migrating from 0.8.X to 0.9.X

Migrating from 0.6.X to 0.7.X

Migrating from 0.5.X to 0.6.X

Migrating from 0.2.X to 0.3.X

Migrating from 0.1.X to 0.2.X

    « API Reference

    Versions »

<form class="navbar-form navbar-right" action="search.html" method="get">

  <input type="text" name="q" class="form-control" placeholder="Search" />

  <input type="hidden" name="check_keywords" value="yes" />
  <input type="hidden" name="area" value="default" />
</form>

Version Migration¶

Migrating from 1.2.X to 1.3.X¶

There are some breaking features:

1 - Security models have changed, user’s can have multiple roles, not just one. So you have to upgrade your db.

The security models schema have changed.

If you are using sqlite, mysql, pgsql, mssql or oracle, use the following procedure:

1 - Backup your DB.

2 - If you haven’t already, upgrade to flask-appbuilder 1.3.0.

3 - Issue the following commands, on your project folder where config.py exists:

$ cd /your-main-project-folder/
$ fabmanager upgrade-db

4 - Test and Run (if you have a run.py for development)

$ fabmanager run

For sqlite you’ll have to drop role_id columns and FK yourself. follow the script instructions to finish the upgrade.

2 - Security. If you were already extending security, this is even more encouraged from now on, but internally many things have
changed. So, modules have changes and changed place, each backend engine will have it’s SecurityManager, and views
are common to all of them. Change:

from:

from flask.ext.appbuilder.security.sqla.views import UserDBModelView
from flask.ext.appbuilder.security.manager import SecurityManager

to:

from flask.ext.appbuilder.security.views import UserDBModelView
from flask.ext.appbuilder.security.sqla.manager import SecurityManager

3 - SQLAInteface, SQLAModel. If you were importing like the following, change:

from:

from flask.ext.appbuilder.models import SQLAInterface

to:

from flask.ext.appbuilder.models.sqla.interface import SQLAInterface

4 - Filters, filters import moved:

to:

from flask_appbuilder.models.sqla.filters import FilterStartsWith, FilterEqualFunction, FilterEqual

5 - Filters, filtering relationship fields (rendered with select2) changed:

from:

edit_form_query_rel_fields = [('group',
                               SQLAModel(Model1, self.db.session),
                               [['field_string', FilterEqual, 'G2']]
                              )
                            ]

to:

edit_form_query_rel_fields = {'group':[['field_string', FilterEqual, 'G2']]}

Migrating from 1.1.X to 1.2.X¶

There is a breaking feature, change your filters import like this:

from:

flask.ext.appbuilder.models.base import Filters, BaseFilter, BaseFilterConverter
flask.ext.appbuilder.models.filters import FilterEqual, FilterRelation ....

to:

flask.ext.appbuilder.models.filters import Filters, BaseFilter, BaseFilterConverter
flask.ext.appbuilder.models.sqla.filter import FilterEqual, FilterRelation ....

Migrating from 0.9.X to 0.10.X¶

This new version has NO breaking features, all your code will work, unless you are hacking directly onto SQLAModel,
Filters, DataModel etc.

But, to keep up with the changes, you should change these:

from flask.ext.appbuilder.models.datamodel import SQLAModel
from flask.ext.appbuilder.models.filters import FilterEqual, FilterContains

to:

from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
from flask.ext.appbuilder.models.sqla.filters import FilterEqual, FilterContains

Migrating from 0.8.X to 0.9.X¶

This new version has a breaking feature, the way you initialize AppBuilder (former BaseApp) has changed.
internal retro compatibility was created, but many things have changed

1 - Initialization of AppBuilder (BaseApp) has changed, pass session not SQLAlchemy db object.
this is the breaking feature.

from (__init__.py)

BaseApp(app, db)

to (__init__.py)

AppBuilder(app, db.session)

2 - ‘BaseApp’ changed to ‘AppBuilder’. Has you already noticed on 1.

3 - BaseApp or now AppBuilder will not automatically create your models, after declaring them just invoke create_db method:

appbuilder.create_db()

4 - Change your models inheritance

from:

class MyModel(Model):
    id = Column(Integer, primary_key=True)
    first_name = Column(String(64), nullable=False)

to:

class MyModel(Model):
    id = Column(Integer, primary_key=True)
    first_name = Column(String(64), nullable=False)

5 - Although your not obligated, you should not directly use your flask.ext.sqlalchemy class SQLAlchemy.
Use F.A.B. SQLA class instead, read the docs to know why.

from (__init__.py):

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.appbuilder.baseapp import BaseApp

app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
baseapp = BaseApp(app, db)

to (__init__.py):

from flask import Flask
from flask.ext.appbuilder import SQLA, AppBuilder

app = Flask(__name__)
app.config.from_object('config')
db = SQLA(app)
appbuilder = AppBuilder(app, db.session)

Migrating from 0.6.X to 0.7.X¶

This new version has some breaking features. You don’t have to change any code, main breaking changes are:

The security models schema have changed.

If you are using sqlite, mysql or pgsql, use the following procedure:

1 - Backup your DB.

2 - If you haven’t already, upgrade to flask-appbuilder 0.7.0.

3 - Issue the following commands, on your project folder where config.py exists:

cd /your-main-project-folder/
wget https://raw.github.com/dpgaspar/Flask-AppBuilder/master/bin/migrate_db_0.7.py
python migrate_db_0.7.py
wget https://raw.github.com/dpgaspar/Flask-AppBuilder/master/bin/hash_db_password.py
python hash_db_password.py

4 - Test and Run (if you have a run.py for development)

python run.py

If not (DB is not sqlite, mysql or pgsql), you will have to alter the schema your self. use the following procedure:

1 - Backup your DB.

2 - If you haven’t already, upgrade to flask-appbuilder 0.7.0.

3 - issue the corresponding DDL commands to:

ALTER TABLE ab_user MODIFY COLUMN password VARCHAR(256)

ALTER TABLE ab_user ADD COLUMN login_count INTEGER

ALTER TABLE ab_user ADD COLUMN created_on DATETIME

ALTER TABLE ab_user ADD COLUMN changed_on DATETIME

ALTER TABLE ab_user ADD COLUMN created_by_fk INTEGER

ALTER TABLE ab_user ADD COLUMN changed_by_fk INTEGER

ALTER TABLE ab_user ADD COLUMN last_login DATETIME

ALTER TABLE ab_user ADD COLUMN fail_login_count INTEGER

4 - Then hash your passwords:

wget https://raw.github.com/dpgaspar/Flask-AppBuilder/master/bin/hash_db_password.py
python hash_db_password.py

All passwords are kept on the database hashed, so all your passwords will be hashed by the framework.

Please backup your DB before altering the schema,  if you feel lost please post an issue on github

https://github.com/dpgaspar/Flask-AppBuilder/issues?state=open

Migrating from 0.5.X to 0.6.X¶

This new version has some breaking features, that i hope will be easily changeable on your code.

If you feel lost please post an issue on github: https://github.com/dpgaspar/Flask-AppBuilder/issues?state=open

If your using the related_views attribute on ModelView classes, you must not instantiate the related classes. This is the correct form, it will be less memory and cpu resource consuming.

From this:

class MyView(GeneralView):
    datamodel = SQLAModel(Group, db.session)
    related_views = [MyOtherView()]

Change to this:

class MyView(GeneralView):
    datamodel = SQLAModel(Group, db.session)
    related_views = [MyOtherView]

Migrating from 0.2.X to 0.3.X¶

This new version (0.3.X) has many internal changes, if you feel lost please post an issue on github
https://github.com/dpgaspar/Flask-AppBuilder/issues?state=open

All direct imports from your ‘app’ directory were removed, so there is no obligation in using the base AppBuilder-Skeleton.

Security tables have changed their names, AppBuilder will automatically migrate all your data to the new tables.

1 - Change your BaseApp initialization (views.py)

From this:

baseapp = BaseApp(app)

Change to this:

baseapp = BaseApp(app, db)

2 - Remove from OpenID and Login initialization (__init__.py)

From this:

app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
babel = Babel(app)
lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'
oid = OpenID(app, os.path.join(basedir, 'tmp'))

from app import models, views

Change to this:

app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)

from app import models, views

Migrating from 0.1.X to 0.2.X¶

It’s very simple, change this:

baseapp = BaseApp(app)
baseapp.add_view(GroupGeneralView, "List Groups","/groups/list","th-large","Contacts")
baseapp.add_view(PersonGeneralView, "List Contacts","/persons/list","earphone","Contacts")
baseapp.add_view(PersonChartView, "Contacts Chart","/persons/chart","earphone","Contacts")

To this:

baseapp = BaseApp(app)
baseapp.add_view(GroupGeneralView(), "List Groups","/groups/list","th-large","Contacts")
baseapp.add_view(PersonGeneralView(), "List Contacts","/persons/list","earphone","Contacts")
baseapp.add_view(PersonChartView(), "Contacts Chart","/persons/chart","earphone","Contacts")

Small change you just have to instantiate your classes.

      Back to top

        © Copyright 2013, Daniel Vaz Gaspar.

      Created using Sphinx 1.3.1.