Qt 6.x
The Qt SDK
Loading...
Searching...
No Matches
qqmldomcomments.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "qqmldomcomments_p.h"
7#include "qqmldomelements_p.h"
11
12#include <QtQml/private/qqmljsastvisitor_p.h>
13#include <QtQml/private/qqmljsast_p.h>
14#include <QtQml/private/qqmljslexer_p.h>
15
16#include <QtCore/QSet>
17
18#include <variant>
19
20static Q_LOGGING_CATEGORY(commentsLog, "qt.qmldom.comments", QtWarningMsg);
21
23namespace QQmlJS {
24namespace Dom {
25
68CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment)
69{
70 commentBegin = 0;
73 hasStartNewline = true;
75 }
77 QString expectedEnd;
78 switch (rawComment.at(commentBegin).unicode()) {
79 case '/':
81 if (commentStartStr == u"/*") {
82 expectedEnd = QStringLiteral(u"*/");
83 } else {
84 if (commentStartStr == u"//") {
85 expectedEnd = QStringLiteral(u"\n");
86 } else {
87 warnings.append(tr("Unexpected comment start %1").arg(commentStartStr));
88 }
89 }
90 break;
91 case '#':
93 expectedEnd = QStringLiteral(u"\n");
94 break;
95 default:
97 warnings.append(tr("Unexpected comment start %1").arg(commentStartStr));
98 break;
99 }
101 quint32 rawEnd = quint32(rawComment.size());
103 QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(0) : expectedEnd.at(0));
104 while (commentEnd < rawEnd) {
106 if (c == e1) {
107 if (expectedEnd.size() > 1) {
108 if (++commentEnd < rawEnd && rawComment.at(commentEnd) == expectedEnd.at(1)) {
109 Q_ASSERT(expectedEnd.size() == 2);
111 break;
112 } else {
114 }
115 } else {
116 // Comment ends with \n, treat as it is not part of the comment but post whitespace
118 break;
119 }
120 } else if (!c.isSpace()) {
122 } else if (c == QLatin1Char('\n')) {
124 } else if (c == QLatin1Char('\r')) {
125 if (expectedEnd == QStringLiteral(u"\n")) {
126 if (commentEnd + 1 < rawEnd
127 && rawComment.at(commentEnd + 1) == QLatin1Char('\n')) {
128 ++commentEnd;
130 } else {
132 }
133 break;
134 } else if (commentEnd + 1 == rawEnd
135 || rawComment.at(commentEnd + 1) != QLatin1Char('\n')) {
137 }
138 }
139 ++commentEnd;
140 }
141
142 if (commentEnd > 0
143 && (rawComment.at(commentEnd - 1) == QLatin1Char('\n')
144 || rawComment.at(commentEnd - 1) == QLatin1Char('\r')))
145 hasEndNewline = true;
147 while (i < rawEnd && rawComment.at(i).isSpace()) {
148 if (rawComment.at(i) == QLatin1Char('\n') || rawComment.at(i) == QLatin1Char('\r'))
149 hasEndNewline = true;
150 ++i;
151 }
152 if (i < rawEnd) {
153 warnings.append(tr("Non whitespace char %1 after comment end at %2")
154 .arg(rawComment.at(i))
155 .arg(i));
156 }
157 }
158}
159
194{
195 bool cont = true;
196 cont = cont && self.dvValueField(visitor, Fields::rawComment, rawComment());
197 cont = cont && self.dvValueField(visitor, Fields::newlinesBefore, newlinesBefore());
198 return cont;
199}
200
201void Comment::write(OutWriter &lw, SourceLocation *commentLocation) const
202{
203 if (newlinesBefore())
205 CommentInfo cInfo = info();
206 lw.ensureSpace(cInfo.preWhitespace());
207 QStringView cBody = cInfo.comment();
208 PendingSourceLocationId cLoc = lw.lineWriter.startSourceLocation(commentLocation);
209 lw.write(cBody.mid(0, 1));
210 bool indentOn = lw.indentNextlines;
211 lw.indentNextlines = false;
212 lw.write(cBody.mid(1));
213 lw.indentNextlines = indentOn;
215 lw.write(cInfo.postWhitespace());
216}
217
239{
240 bool cont = true;
241 cont = cont && self.dvWrapField(visitor, Fields::preComments, preComments);
242 cont = cont && self.dvWrapField(visitor, Fields::postComments, postComments);
243 return cont;
244}
245
247{
248 if (locs)
249 locs->resize(preComments.size());
250 int i = 0;
251 for (const Comment &c : preComments)
252 c.write(lw, (locs ? &((*locs)[i++]) : nullptr));
253}
254
256{
257 if (locs)
258 locs->resize(postComments.size());
259 int i = 0;
260 for (const Comment &c : postComments)
261 c.write(lw, (locs ? &((*locs)[i++]) : nullptr));
262}
263
274{
276 { { elLocation.begin() * 2, &preComments },
277 { elLocation.end() * 2 + 1, &postComments } });
278}
279
280using namespace QQmlJS::AST;
281
283{
284public:
285 Path path; // store the MutableDomItem instead?
287};
288
289// internal class to keep a reference either to an AST::Node* or a region of a DomItem and the
290// size of that region
292{
293public:
296 : element(RegionRef { path, region }), size(size)
297 {
298 }
299 operator bool() const
300 {
301 return (element.index() == 0 && std::get<0>(element)) || element.index() == 1 || size != 0;
302 }
303 ElementRef() = default;
304
305 std::variant<AST::Node *, RegionRef> element;
307};
308
325{
329
333#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
335#endif
341 return res;
342}
343
344// internal private class to set all the starts/ends of the nodes/regions
345class AstRangesVisitor final : protected VisitAll
346{
347public:
348 AstRangesVisitor() = default;
349
350 void addNodeRanges(AST::Node *rootNode);
351 void addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP);
352
353 void throwRecursionDepthError() override { }
354
355 static const QSet<int> kindsToSkip();
356
357 bool preVisit(Node *n) override
358 {
359 if (!kindsToSkip().contains(n->kind)) {
360 quint32 start = n->firstSourceLocation().begin();
361 quint32 end = n->lastSourceLocation().end();
362 if (!starts.contains(start))
363 starts.insert(start, { n, end - start });
364 if (!ends.contains(end))
365 ends.insert(end, { n, end - start });
366 }
367 return true;
368 }
369
374};
375
377{
378 AST::Node::accept(rootNode, this);
379}
380
382{
383 if (!itemLocations) {
384 if (item)
385 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
386 return;
387 }
388 DomItem comments = item.field(Fields::comments);
389 if (comments) {
390 auto regs = itemLocations->info().regions;
391 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
392 quint32 startI = it.value().begin();
393 quint32 endI = it.value().end();
394 if (!starts.contains(startI))
395 starts.insert(startI, { currentP, it.key(), quint32(endI - startI) });
396 if (!ends.contains(endI))
397 ends.insert(endI, { currentP, it.key(), endI - startI });
398 }
399 }
400 {
401 auto subMaps = itemLocations->subItems();
402 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
403 addItemRanges(item.path(it.key()),
404 std::static_pointer_cast<AttachedInfoT<FileLocations>>(it.value()),
405 currentP.path(it.key()));
406 }
407 }
408}
409
410const QSet<int> AstRangesVisitor::kindsToSkip()
411{
412 static QSet<int> res = QSet<int>({
413 AST::Node::Kind_ArgumentList,
414 AST::Node::Kind_ElementList,
415 AST::Node::Kind_FormalParameterList,
416 AST::Node::Kind_ImportsList,
417 AST::Node::Kind_ExportsList,
418 AST::Node::Kind_PropertyDefinitionList,
419 AST::Node::Kind_StatementList,
420 AST::Node::Kind_VariableDeclarationList,
421 AST::Node::Kind_ClassElementList,
422 AST::Node::Kind_PatternElementList,
423 AST::Node::Kind_PatternPropertyList,
424 AST::Node::Kind_TypeArgument,
425 })
426 .unite(VisitAll::uiKinds());
427 return res;
428}
429
435bool AstComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
436{
437 bool cont = self.dvItemField(visitor, Fields::commentedElements, [this, &self]() {
438 return self.subMapItem(Map(
439 self.pathFromOwner().field(Fields::commentedElements),
440 [this](DomItem &map, QString key) {
441 bool ok;
442 // we expose the comments as map just for debugging purposes,
443 // as key we use the address hex value as key (keys must be strings)
444 quintptr v = key.split(QLatin1Char('_')).last().toULong(&ok, 16);
445 // recover the actual key, and check if it is in the map
446 AST::Node *n = reinterpret_cast<AST::Node *>(v);
447 if (ok && m_commentedElements.contains(n))
448 return map.wrap(PathEls::Key(key), m_commentedElements[n]);
449 return DomItem();
450 },
451 [this](DomItem &) {
452 QSet<QString> res;
453 for (AST::Node *n : m_commentedElements.keys()) {
454 QString name;
455 if (n)
456 name = QString::number(n->kind); // we should add mapping to
457 // string for this
458 res.insert(name + QStringLiteral(u"_") + QString::number(quintptr(n), 16));
459 }
460 return res;
461 },
462 QLatin1String("CommentedElements")));
463 });
464 return cont;
465}
466
467void AstComments::collectComments(MutableDomItem &item)
468{
469 if (std::shared_ptr<ScriptExpression> scriptPtr = item.ownerAs<ScriptExpression>()) {
470 DomItem itemItem = item.item();
471 return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments(),
472 item, FileLocations::treeOf(itemItem));
473 } else if (std::shared_ptr<QmlFile> qmlFilePtr = item.ownerAs<QmlFile>()) {
474 return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments(),
475 item, qmlFilePtr->fileLocationsTree());
476 } else {
477 qCWarning(commentsLog)
478 << "collectComments works with QmlFile and ScriptExpression, not with"
479 << item.internalKindStr();
480 }
481}
482
488void AstComments::collectComments(std::shared_ptr<Engine> engine, AST::Node *n,
489 std::shared_ptr<AstComments> ccomm, MutableDomItem rootItem,
490 FileLocations::Tree rootItemLocations)
491{
492 if (!n)
493 return;
494 AstRangesVisitor ranges;
495 ranges.addItemRanges(rootItem.item(), rootItemLocations, Path());
496 ranges.addNodeRanges(n);
497 QStringView code = engine->code();
498 QHash<AST::Node *, CommentedElement> &commentedElements = ccomm->m_commentedElements;
499 quint32 lastPostCommentPostEnd = 0;
500 for (SourceLocation cLoc : engine->comments()) {
501 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
502 // do not add newline before, but add the one after
503 quint32 iPre = cLoc.begin();
504 int preNewline = 0;
505 int postNewline = 0;
506 QStringView commentStartStr;
507 while (iPre > 0) {
508 QChar c = code.at(iPre - 1);
509 if (!c.isSpace()) {
510 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
511 && iPre - 1 > 0 && code.at(iPre - 2) == QLatin1Char('/')) {
512 commentStartStr = code.mid(iPre - 2, 2);
513 --iPre;
514 } else {
515 break;
516 }
517 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
518 preNewline = 1;
519 // possibly add an empty line if it was there (but never more than one)
520 int i = iPre - 1;
521 if (c == QLatin1Char('\n') && i > 0 && code.at(i - 1) == QLatin1Char('\r'))
522 --i;
523 while (i > 0 && code.at(--i).isSpace()) {
524 c = code.at(i);
525 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
526 ++preNewline;
527 break;
528 }
529 }
530 break;
531 }
532 --iPre;
533 }
534
535 if (iPre == 0)
536 preNewline = 1;
537
538 qsizetype iPost = cLoc.end();
539 while (iPost < code.size()) {
540 QChar c = code.at(iPost);
541 if (!c.isSpace()) {
542 if (!commentStartStr.isEmpty() && commentStartStr.at(1) == QLatin1Char('*')
543 && c == QLatin1Char('*') && iPost + 1 < code.size()
544 && code.at(iPost + 1) == QLatin1Char('/')) {
545 commentStartStr = QStringView();
546 ++iPost;
547 } else {
548 break;
549 }
550 } else {
551 if (c == QLatin1Char('\n')) {
552 ++postNewline;
553 if (iPost + 1 < code.size() && code.at(iPost + 1) == QLatin1Char('\n')) {
554 ++iPost;
555 ++postNewline;
556 }
557 } else if (c == QLatin1Char('\r')) {
558 if (iPost + 1 < code.size() && code.at(iPost + 1) == QLatin1Char('\n')) {
559 ++iPost;
560 ++postNewline;
561 }
562 }
563 }
564 ++iPost;
565 if (postNewline > 1)
566 break;
567 }
568
569 ElementRef commentEl;
570 bool pre = true;
571 auto iStart = ranges.starts.lowerBound(cLoc.begin());
572 auto iEnd = ranges.ends.lowerBound(cLoc.begin());
573 Q_ASSERT(!ranges.ends.isEmpty() && !ranges.starts.isEmpty());
574
575 auto checkElementBefore = [&]() {
576 if (commentEl)
577 return;
578 // prefer post comment attached to preceding element
579 auto preEnd = iEnd;
580 auto preStart = iStart;
581 if (preEnd != ranges.ends.begin()) {
582 --preEnd;
583 if (iStart == ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
584 // iStart == begin should never happen
585 // check that we do not have operators (or in general other things) between
586 // preEnd and this because inserting a newline too ealy might invalidate the
587 // expression (think a + //comment\n b ==> a // comment\n + b), in this
588 // case attaching as preComment of iStart (b in the example) should be
589 // preferred as it is safe
590 quint32 i = iPre;
591 while (i != 0 && code.at(--i).isSpace())
592 ;
593 if (i <= preEnd.key() || i < lastPostCommentPostEnd
594 || iEnd == ranges.ends.end()) {
595 commentEl = preEnd.value();
596 pre = false;
597 lastPostCommentPostEnd = iPost + 1; // ensure the previous check works
598 // with multiple post comments
599 }
600 }
601 }
602 };
603 auto checkElementAfter = [&]() {
604 if (commentEl)
605 return;
606 if (iStart != ranges.starts.end()) {
607 // try to add a pre comment of following element
608 if (iEnd == ranges.ends.end() || iEnd.key() > iStart.key()) {
609 // there is no end of element before iStart begins
610 // associate the comment as preComment of iStart
611 // (btw iEnd == end should never happen here)
612 commentEl = iStart.value();
613 return;
614 }
615 }
616 if (iStart == ranges.starts.begin()) {
617 Q_ASSERT(iStart != ranges.starts.end());
618 // we are before the first node (should be handled already by previous case)
619 commentEl = iStart.value();
620 }
621 };
622 auto checkInsideEl = [&]() {
623 if (commentEl)
624 return;
625 auto preIStart = iStart;
626 if (iStart == ranges.starts.begin()) {
627 commentEl = iStart.value(); // checkElementAfter should have handled this
628 return;
629 } else {
630 --preIStart;
631 }
632 // we are inside a node, actually inside both n1 and n2 (which might be the same)
633 // add to pre of the smallest between n1 and n2.
634 // This is needed because if there are multiple nodes starting/ending at the same
635 // place we store only the first (i.e. largest)
636 ElementRef n1 = preIStart.value();
637 ElementRef n2 = iEnd.value();
638 if (n1.size > n2.size)
639 commentEl = n2;
640 else
641 commentEl = n1;
642 };
643 if (!preNewline) {
644 checkElementBefore();
645 checkElementAfter();
646 } else {
647 checkElementAfter();
648 checkElementBefore();
649 }
650 if (!commentEl)
651 checkInsideEl();
652 if (!commentEl) {
653 qCWarning(commentsLog) << "Could not assign comment at" << locationToData(cLoc)
654 << "adding before root node";
655 if (rootItem && (rootItemLocations || !n)) {
656 commentEl.element = RegionRef { Path(), QString() };
657 commentEl.size =
658 rootItemLocations->info()
659 .regions.value(QString(), rootItemLocations->info().fullRegion)
660 .length;
661 // attach to rootItem
662 } else if (n) {
663 commentEl.element = n;
664 commentEl.size = n->lastSourceLocation().end() - n->firstSourceLocation().begin();
665 }
666 }
667
668 Comment comment(code.mid(iPre, iPost - iPre), preNewline);
669 if (commentEl.element.index() == 0 && std::get<0>(commentEl.element)) {
670 CommentedElement &cEl = commentedElements[std::get<0>(commentEl.element)];
671 if (pre)
672 cEl.preComments.append(comment);
673 else
674 cEl.postComments.append(comment);
675 } else if (commentEl.element.index() == 1) {
676 DomItem rComments = rootItem.item()
677 .path(std::get<1>(commentEl.element).path)
678 .field(Fields::comments);
679 if (RegionComments *rCommentsPtr = rComments.mutableAs<RegionComments>()) {
680 if (pre)
681 rCommentsPtr->addPreComment(comment, std::get<1>(commentEl.element).regionName);
682 else
683 rCommentsPtr->addPostComment(comment,
684 std::get<1>(commentEl.element).regionName);
685 } else {
686 Q_ASSERT(false);
687 }
688 } else {
689 qCWarning(commentsLog)
690 << "Failed: no item or node to attach comment" << comment.rawComment();
691 }
692 }
693}
694
695// internal class to collect all comments in a node or its subnodes
697{
698public:
699 CommentCollectorVisitor(AstComments *comments, AST::Node *n) : comments(comments)
700 {
701 AST::Node::accept(n, this);
702 }
703
704 void throwRecursionDepthError() override { }
705
706 bool preVisit(Node *n) override
707 {
708 auto &cEls = comments->commentedElements();
709 if (cEls.contains(n))
710 nodeComments += cEls[n].commentGroups(
711 combine(n->firstSourceLocation(), n->lastSourceLocation()));
712 return true;
713 }
714
717};
718
725QMultiMap<quint32, const QList<Comment> *> AstComments::allCommentsInNode(AST::Node *n)
726{
728 return v.nodeComments;
729}
730
731bool RegionComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
732{
733 bool cont = true;
734 if (!regionComments.isEmpty())
735 cont = cont && self.dvWrapField(visitor, Fields::regionComments, regionComments);
736 return cont;
737}
738
739} // namespace Dom
740} // namespace QQmlJS
\inmodule QtCore
Definition qatomic.h:112
\inmodule QtCore
Definition qchar.h:48
constexpr char16_t unicode() const noexcept
Returns the numeric Unicode value of the QChar.
Definition qchar.h:458
static constexpr QChar fromLatin1(char c) noexcept
Converts the Latin-1 character c to its equivalent QChar.
Definition qchar.h:461
constexpr bool isSpace() const noexcept
Returns true if the character is a separator character (Separator_* categories or certain code points...
Definition qchar.h:466
\inmodule QtCore
Definition qhash.h:818
iterator begin()
Returns an \l{STL-style iterators}{STL-style iterator} pointing to the first item in the hash.
Definition qhash.h:1202
Definition qlist.h:74
void resize(qsizetype size)
Definition qlist.h:392
Definition qmap.h:186
void accept(BaseVisitor *visitor)
Associates comments with AST::Node *.
void addNodeRanges(AST::Node *rootNode)
bool preVisit(Node *n) override
QMap< quint32, ElementRef > ends
void addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP)
static const QSet< int > kindsToSkip()
QMap< quint32, ElementRef > starts
FileLocations::Tree rootItemLocations
CommentCollectorVisitor(AstComments *comments, AST::Node *n)
QMultiMap< quint32, const QList< Comment > * > nodeComments
Extracts various pieces and information out of a rawComment string.
QStringView postWhitespace() const
QStringView comment() const
QStringView preWhitespace() const
Represents a comment.
void write(OutWriter &lw, SourceLocation *commentLocation=nullptr) const
bool iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
Expose attributes to the Dom.
QStringView rawComment() const
CommentInfo info() const
Keeps the comment associated with an element.
void writePre(OutWriter &lw, QList< SourceLocation > *locations=nullptr) const
bool iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
void writePost(OutWriter &lw, QList< SourceLocation > *locations=nullptr) const
QMultiMap< quint32, const QList< Comment > * > commentGroups(SourceLocation elLocation) const
Given the SourceLocation of the current element returns the comments associated with the start and en...
ElementRef(AST::Node *node, quint32 size)
ElementRef(Path path, QString region, quint32 size)
std::variant< AST::Node *, RegionRef > element
std::shared_ptr< AttachedInfoT< FileLocations > > Tree
void endSourceLocation(PendingSourceLocationId)
PendingSourceLocationId startSourceLocation(SourceLocation *)
OutWriter & ensureNewline(int nNewlines=1)
OutWriter & write(QStringView v, LineWriter::TextAddType t=LineWriter::TextAddType::Normal)
Keeps the comments associated with a DomItem.
A vistor that visits all the AST:Node.
static QSet< int > uiKinds()
returns a set with all Ui* Nodes (i.e.
Definition qset.h:18
iterator begin()
Definition qset.h:136
iterator end()
Definition qset.h:140
const_iterator cbegin() const noexcept
Definition qset.h:138
\inmodule QtCore
Definition qstringview.h:76
constexpr bool isEmpty() const noexcept
Returns whether this string view is empty - that is, whether {size() == 0}.
constexpr qsizetype size() const noexcept
Returns the size of this string view, in UTF-16 code units (that is, surrogate pairs count as two for...
constexpr QChar at(qsizetype n) const noexcept
Returns the character at position n in this string view.
constexpr QStringView mid(qsizetype pos, qsizetype n=-1) const noexcept
Returns the substring of length length starting at position start in this object.
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
qsizetype size() const
Returns the number of characters in this string.
Definition qstring.h:182
QString mid(qsizetype position, qsizetype n=-1) const
Returns a string that contains n characters of this string, starting at the specified position index.
Definition qstring.cpp:5204
const QChar at(qsizetype i) const
Returns the character at the given index position in the string.
Definition qstring.h:1079
bool isEmpty() const
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:1083
QMap< QString, QString > map
[6]
QSet< QString >::iterator it
QMLDOM_EXPORT QCborValue locationToData(SourceLocation loc, QStringView strValue=u"")
Combined button and popup list for selecting options.
@ QtWarningMsg
Definition qlogging.h:31
#define Q_LOGGING_CATEGORY(name,...)
#define qCWarning(category,...)
static bool contains(const QJsonArray &haystack, unsigned needle)
Definition qopengl.cpp:116
GLsizei const GLfloat * v
[13]
GLuint64 key
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLuint GLuint end
GLuint start
GLfloat n
GLuint res
const GLubyte * c
GLsizei const GLchar *const * path
const QQuickItem * rootItem(const I &item)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
SSL_CTX int(*) void arg)
QLatin1StringView QLatin1String
Definition qstringfwd.h:31
#define QStringLiteral(str)
#define tr(X)
unsigned int quint32
Definition qtypes.h:45
ptrdiff_t qsizetype
Definition qtypes.h:70
QList< QPair< QString, QString > > Map
QGraphicsItem * item
QJSEngine engine
[0]
\inmodule QtCore \reentrant
Definition qchar.h:17