Merge pull request #8397 from ErikMelton/unauthorized-http-get-requests-from-notifications
Require textFormat declaration for all Text elements
This commit is contained in:
@@ -138,6 +138,7 @@ BorderSurface {
|
||||
radius: 0
|
||||
}
|
||||
contentItem: Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.tooltipText
|
||||
color: root.tooltipForeground
|
||||
font.family: root.fontFamily
|
||||
@@ -158,6 +159,7 @@ BorderSurface {
|
||||
spacing: Style.spacing.controlGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.iconText !== ""
|
||||
text: root.iconText
|
||||
color: root.selected ? root._selectedColor : root.foreground
|
||||
@@ -177,6 +179,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.text !== ""
|
||||
text: root.text
|
||||
color: root.selected ? root._selectedColor : root.foreground
|
||||
|
||||
@@ -69,6 +69,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: messageText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
@@ -105,6 +106,7 @@ Item {
|
||||
radius: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
|
||||
|
||||
@@ -71,6 +71,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -110,6 +111,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -214,6 +216,7 @@ Item {
|
||||
: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -259,6 +259,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -298,6 +299,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -451,6 +453,7 @@ Item {
|
||||
: Border.controlSpec("normal", root.foreground, root.accent)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.loadingOptions ? "" : ""
|
||||
color: root.foreground
|
||||
@@ -486,6 +489,7 @@ Item {
|
||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: resultList.count === 0
|
||||
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
|
||||
@@ -581,6 +585,7 @@ Item {
|
||||
spacing: Style.spacing.xxs
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.label
|
||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -589,6 +594,7 @@ Item {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: modelData.description
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -25,6 +25,7 @@ Column {
|
||||
spacing: Style.spacing.md
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
|
||||
@@ -25,6 +25,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
textFormat: Text.PlainText
|
||||
// Keep the shared line box and baseline intact. Correcting only the
|
||||
// horizontal painted bounds avoids per-glyph vertical drift.
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -69,6 +69,7 @@ BorderSurface {
|
||||
Behavior on color { ColorAnimation { duration: 60 } }
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.iconText
|
||||
color: root.enabled
|
||||
|
||||
@@ -48,6 +48,7 @@ Item {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
|
||||
@@ -75,6 +76,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: detailText
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.detail
|
||||
color: root.dim
|
||||
@@ -87,6 +89,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: metaText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: root.meta.toUpperCase()
|
||||
visible: text !== ""
|
||||
|
||||
@@ -11,6 +11,10 @@ Text {
|
||||
property string fontFamily: Style.font.family
|
||||
property real fontSize: Style.font.caption
|
||||
|
||||
// Callers bind `text` from outside this file, so the default has to be set
|
||||
// here. AutoText would let a section title that happens to carry a device or
|
||||
// network name promote itself to rich text.
|
||||
textFormat: Text.PlainText
|
||||
color: Qt.darker(foreground, 1.4)
|
||||
font.family: fontFamily
|
||||
font.pixelSize: fontSize
|
||||
|
||||
@@ -36,6 +36,7 @@ ToolTip {
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.text
|
||||
color: root.panelForeground
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -93,6 +93,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -132,6 +133,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -246,6 +248,7 @@ Item {
|
||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: resultList.count === 0
|
||||
text: root.emptyText
|
||||
@@ -313,6 +316,7 @@ Item {
|
||||
spacing: Style.spacing.xxs
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.optionLabel(modelData)
|
||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -321,6 +325,7 @@ Item {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: root.optionDescription(modelData)
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -130,6 +130,7 @@ PanelWindow {
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.title !== ""
|
||||
text: root.title.toUpperCase()
|
||||
color: root.onScrimDim
|
||||
@@ -182,6 +183,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.failed
|
||||
text: root.error
|
||||
color: root.onScrimUrgent
|
||||
@@ -368,6 +370,7 @@ PanelWindow {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
// Both branches go through the locale: a reading is a measurement, so
|
||||
// its separators follow the system's number conventions rather than the
|
||||
@@ -383,6 +386,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.unit
|
||||
color: root.onScrimDim
|
||||
@@ -394,6 +398,7 @@ PanelWindow {
|
||||
// The 90° gap at the bottom of the scale is where a cluster prints its
|
||||
// unit; here it names the direction.
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
text: dial.label
|
||||
|
||||
@@ -69,6 +69,7 @@ BorderSurface {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.label
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -79,6 +80,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.description !== ""
|
||||
text: root.description
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -74,6 +74,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: label
|
||||
textFormat: Text.PlainText
|
||||
visible: root.labelVisible
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
|
||||
@@ -434,6 +434,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: heroMarkImage.status !== Image.Ready
|
||||
text: button.text
|
||||
@@ -504,6 +505,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: statusText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -558,6 +560,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: balanceValue
|
||||
textFormat: Text.PlainText
|
||||
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
|
||||
color: root.balanceAlarming ? root.urgent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -575,6 +578,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
width: parent.width
|
||||
text: root.balanceDetailText(root.balance)
|
||||
@@ -680,6 +684,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
width: parent.width
|
||||
topPadding: Style.space(2)
|
||||
@@ -710,6 +715,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: limitLabel
|
||||
textFormat: Text.PlainText
|
||||
// A model-scoped window is titled after its model, and those names run
|
||||
// long enough to reach the percentage, so the title gives way first.
|
||||
text: limitRow.window ? limitRow.window.title : ""
|
||||
@@ -725,6 +731,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: limitValue
|
||||
textFormat: Text.PlainText
|
||||
text: limitRow.window && limitRow.window.percent >= 0
|
||||
? Math.round(limitRow.window.percent * 100) + "%"
|
||||
: "—"
|
||||
@@ -744,6 +751,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: resetText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: {
|
||||
var remainingMs = root.resetMsFor(limitRow.window)
|
||||
@@ -798,6 +806,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: dayLabel
|
||||
textFormat: Text.PlainText
|
||||
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
|
||||
color: dayRow.today ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -835,6 +844,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: dayValue
|
||||
textFormat: Text.PlainText
|
||||
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
|
||||
color: dayRow.today ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -890,6 +900,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: modelName
|
||||
textFormat: Text.PlainText
|
||||
text: modelRow.row ? modelRow.row.name : ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -904,6 +915,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: modelTokens
|
||||
textFormat: Text.PlainText
|
||||
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -1090,6 +1090,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: tooltipLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.tooltipText
|
||||
color: Color.tooltip.text
|
||||
|
||||
@@ -29,6 +29,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
|
||||
@@ -467,6 +467,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: rowIcon.right
|
||||
anchors.leftMargin: Style.space(10)
|
||||
@@ -577,6 +578,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(28)
|
||||
@@ -681,6 +683,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
@@ -709,6 +712,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -432,6 +432,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -500,6 +501,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
|
||||
height: parent.height
|
||||
text: parent.parent.previewText
|
||||
@@ -546,6 +548,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: parent.activeRow && !parent.activeRow.previewImage
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.contentMargin
|
||||
@@ -593,6 +596,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -519,12 +519,14 @@ Item {
|
||||
width: Style.space(140)
|
||||
spacing: Style.space(1)
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: "Style.font." + modelData.key
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.size + " px"
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
@@ -534,6 +536,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: sampleText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: metaCol.right
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -574,6 +577,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.family
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -587,6 +591,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.resolvedFamily
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -600,6 +605,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.baseSize + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -613,6 +619,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.bar.sizeHorizontal + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -626,6 +633,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.bar.sizeVertical + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -639,6 +647,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.spacing.scale.toFixed(2)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -652,6 +661,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.spacing.panelPadding + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -818,6 +828,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: csLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1273,6 +1284,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -247,6 +247,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -284,6 +285,7 @@ Item {
|
||||
color: hasCursor ? root.selectedBackground : "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: parent.emoji
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
@@ -326,6 +328,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: "No matches for “" + root.filterText + "”"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -545,6 +545,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: selectedLabel
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabels
|
||||
anchors.top: carousel.bottom
|
||||
anchors.topMargin: Style.space(16)
|
||||
@@ -561,6 +562,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.filterable && root.filterText
|
||||
anchors.top: selectedLabel.bottom
|
||||
anchors.topMargin: Style.space(8)
|
||||
|
||||
@@ -184,6 +184,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: passwordInput
|
||||
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
||||
visible: passwordInput.text.length === 0
|
||||
|
||||
@@ -1199,6 +1199,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1287,6 +1288,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
textFormat: Text.PlainText
|
||||
visible: row.hasIcon && !row.isApp
|
||||
text: row.icon
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
@@ -1328,6 +1330,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: row.label
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
@@ -1338,6 +1341,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: row.detail
|
||||
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
|
||||
@@ -1358,6 +1362,7 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: false
|
||||
text: row.childCount
|
||||
color: root.foreground
|
||||
@@ -1368,6 +1373,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: row.kind === "menu" || row.kind === "link" ? "›" : ""
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
|
||||
@@ -1452,6 +1458,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -5,8 +5,92 @@ function isChromiumDerived(app, appIcon) {
|
||||
source.indexOf("opera") >= 0
|
||||
}
|
||||
|
||||
// True when a `<...>` run is an image tag, so the name is read the way Qt's
|
||||
// parser reads it: after the `<`, the leading run of letters and digits.
|
||||
//
|
||||
// Skip everything up to that run rather than matching the separator, because
|
||||
// there is no JavaScript expression for what Qt skips. QQuickStyledText calls
|
||||
// skipSpace(), which is QChar::isSpace(), and that set is not `\s`: Qt counts
|
||||
// U+0085 NEL and `\s` does not, while `\s` counts U+FEFF and Qt does not. A
|
||||
// name read with `\s` therefore misses a tag written as `<`, U+0085, `img`:
|
||||
// Qt skips the NEL, reads `img` and issues the GET, while the regex finds no
|
||||
// name at all and the tag is kept. Measured against Qt 6.11.2.
|
||||
//
|
||||
// Over-skipping is the safe direction. It can only classify more runs as
|
||||
// images, and dropping a run never manufactures a tag: a dropped run joins two
|
||||
// stretches of text that each contain no `<`.
|
||||
function isImageTag(tag) {
|
||||
var name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
|
||||
return !!name && name[1].toLowerCase() === "img"
|
||||
}
|
||||
|
||||
// The body renders as StyledText so notifications can use the markup the
|
||||
// body-markup capability advertises (see Service.qml). StyledText honours
|
||||
// <img src>, and a remote src makes the shell issue an unauthenticated GET
|
||||
// with no user action, so image tags go before the renderer sees them.
|
||||
//
|
||||
// Work in whole tags, never in substrings of one. A `<` opens a tag that runs
|
||||
// to the next `>`, nested `<` and all, and only a tag whose own name is `img`
|
||||
// is dropped.
|
||||
//
|
||||
// That is the conservative bound, not Qt's exact one: Qt lets a `>` inside a
|
||||
// quoted attribute value pass without closing the tag, so a Qt tag can be
|
||||
// longer than the run taken here. Do not "correct" this to match Qt. Taking
|
||||
// the shorter run only ever splits one Qt tag into several, and a split can
|
||||
// only expose an `<img` to be dropped, never hide one — whereas honouring
|
||||
// quotes would let `<b title="a>b"><img src="http://host/x.png">` through.
|
||||
//
|
||||
// Deleting a substring is what makes a naive `/<img[^>]*>/g` unsafe. Given
|
||||
//
|
||||
// <im<img src="http://a/decoy.png">g src="http://a/beacon.png">
|
||||
//
|
||||
// Qt reads ONE malformed tag named `im` and renders nothing, but removing the
|
||||
// inner match closes the surviving halves up into `<img src=".../beacon.png">`
|
||||
// — a live tag the input never contained. The stripper would be manufacturing
|
||||
// the very thing it exists to remove.
|
||||
//
|
||||
// Because every `<` opens a tag, the text between tags never contains one, so
|
||||
// dropping a tag cannot splice its neighbours into a new one. That makes a
|
||||
// single pass sufficient, with no re-scanning and no input bound to police.
|
||||
function stripImageTags(text) {
|
||||
var out = ""
|
||||
var i = 0
|
||||
|
||||
while (i < text.length) {
|
||||
var open = text.indexOf("<", i)
|
||||
if (open === -1) {
|
||||
out += text.slice(i)
|
||||
break
|
||||
}
|
||||
|
||||
out += text.slice(i, open)
|
||||
|
||||
// An unterminated tag at the end of the string still reaches the renderer,
|
||||
// which closes it itself, so treat the remainder as one tag.
|
||||
var close = text.indexOf(">", open)
|
||||
var tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
|
||||
|
||||
if (!isImageTag(tag)) out += tag
|
||||
i = close === -1 ? text.length : close + 1
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// What the card renders, and the last thing to touch the string before Qt parses
|
||||
// it. The newline rewrite belongs here rather than in the card because it inserts
|
||||
// `<br/>` into text stripImageTags chose to KEEP, and a kept tag may hold a `<` of
|
||||
// its own: `<x`, newline, `<img src="http://…">` is one tag named `x` to both the
|
||||
// stripper and Qt, until the rewrite splits it into `<x<br/>` and a live image tag
|
||||
// the input never contained. Measured against Qt 6.11.2 — the rewritten form
|
||||
// fetches, the original does not. So strip again after, and what Qt parses is what
|
||||
// was checked last.
|
||||
function styledBody(body, app, appIcon) {
|
||||
return stripImageTags(sanitizeBody(body, app, appIcon).replace(/\r\n|\r|\n/g, "<br/>"))
|
||||
}
|
||||
|
||||
function sanitizeBody(body, app, appIcon) {
|
||||
var text = String(body || "").replace(/<img[^>]*>/gi, "")
|
||||
var text = stripImageTags(String(body || ""))
|
||||
if (!isChromiumDerived(app, appIcon)) return text
|
||||
|
||||
return text
|
||||
@@ -366,6 +450,7 @@ if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
isChromiumDerived: isChromiumDerived,
|
||||
sanitizeBody: sanitizeBody,
|
||||
styledBody: styledBody,
|
||||
summaryStartsWithGlyph: summaryStartsWithGlyph,
|
||||
shouldBypassDnd: shouldBypassDnd,
|
||||
isEphemeralApp: isEphemeralApp,
|
||||
|
||||
@@ -44,7 +44,7 @@ BorderSurface {
|
||||
readonly property bool singleLineToast: sanitizedBody.length === 0
|
||||
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
|
||||
readonly property string sanitizedBody: sanitizeBody(body)
|
||||
readonly property string styledBody: sanitizedBody.replace(/\r\n|\r|\n/g, "<br/>")
|
||||
readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon)
|
||||
|
||||
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
|
||||
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
|
||||
@@ -133,6 +133,7 @@ BorderSurface {
|
||||
// Glyph fallback (Nerd Font character) when no image icon is
|
||||
// available. Used by omarchy-notification-send's `-g` flag.
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
|
||||
text: root.glyph
|
||||
@@ -143,6 +144,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: root.compactGlyph
|
||||
text: root.glyph
|
||||
@@ -159,6 +161,11 @@ BorderSurface {
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
// The spec defines the summary as a single line of plain text, so
|
||||
// AutoText could only ever promote a hostile string to rich text.
|
||||
// The body below is StyledText on purpose — see Service.qml's
|
||||
// bodyMarkupSupported — and is stripped in NotificationLogic.
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
visible: root.summary.length > 0
|
||||
text: root.summary
|
||||
|
||||
@@ -159,6 +159,7 @@ Item {
|
||||
width: root.iconWidth
|
||||
height: parent.height
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
// Sit the glyph's ink flush in the column, centered when the
|
||||
// column is wider than this particular glyph.
|
||||
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
||||
@@ -186,6 +187,7 @@ Item {
|
||||
}
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.message !== ""
|
||||
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
||||
// The readout hugs the card edge so a short percentage doesn't leave
|
||||
|
||||
@@ -711,6 +711,7 @@ Panel {
|
||||
// Status only — the switch owns muting, mouse and keyboard alike.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.outputIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -761,6 +762,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroLabel
|
||||
textFormat: Text.PlainText
|
||||
text: root.outputVolumeName(
|
||||
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
|
||||
root.outputMuted
|
||||
@@ -800,6 +802,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: outputPercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -886,6 +889,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: microphonePercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1030,6 +1034,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.sinkGlyph(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1040,6 +1045,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.nodeLabel(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1089,6 +1095,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.sourceGlyph(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1099,6 +1106,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.nodeLabel(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1159,6 +1167,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: streamMuteIcon
|
||||
textFormat: Text.PlainText
|
||||
text: streamRow.streamMuted ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1179,6 +1188,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.streamLabel(streamRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1191,6 +1201,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: streamPct
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round(streamRow.streamVolume * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -698,6 +698,7 @@ Panel {
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.icon
|
||||
@@ -748,6 +749,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroStatus
|
||||
textFormat: Text.PlainText
|
||||
text: root.heroStatusText.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -863,6 +865,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
|
||||
text: !root.adapter ? "No Bluetooth adapter"
|
||||
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
||||
@@ -971,6 +974,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: deviceIcon
|
||||
textFormat: Text.PlainText
|
||||
text: row.isConnected ? "" : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -989,6 +993,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.deviceLabel(row.dev) || "Device"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -997,6 +1002,7 @@ Panel {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: row.statusText !== ""
|
||||
text: row.statusText
|
||||
color: row.statusColor
|
||||
|
||||
@@ -311,6 +311,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroDate
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Qt.formatDate(root.today, "MMMM d")
|
||||
color: heroMouse.containsMouse
|
||||
@@ -413,6 +414,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: yearLabel
|
||||
textFormat: Text.PlainText
|
||||
visible: !root.editingLife
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -425,6 +427,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: yearPercent
|
||||
textFormat: Text.PlainText
|
||||
visible: !root.editingLife
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -485,6 +488,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: lifePercent
|
||||
textFormat: Text.PlainText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.lifeDonePercent + "%"
|
||||
@@ -608,6 +612,7 @@ Panel {
|
||||
model: root.weekdays
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
required property var modelData
|
||||
width: root.cellWidth
|
||||
height: Style.space(16)
|
||||
@@ -631,6 +636,7 @@ Panel {
|
||||
spacing: root.cellSpacing
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: root.weekColumnWidth
|
||||
height: root.cellHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -662,6 +668,7 @@ Panel {
|
||||
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: modelData.day
|
||||
color: modelData.inMonth
|
||||
@@ -707,6 +714,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: monthLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Fixed width so the chevrons hold still between a
|
||||
|
||||
@@ -281,6 +281,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
||||
width: parent.width
|
||||
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
||||
@@ -421,6 +422,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
||||
color: root.foreground
|
||||
@@ -430,6 +432,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
||||
color: root.dim
|
||||
@@ -478,6 +481,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Model.fileGlyph(fileRow.fileName)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -491,6 +495,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: fileRow.fileName
|
||||
color: root.foreground
|
||||
@@ -500,6 +505,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: Model.fileMeta(fileRow.file)
|
||||
color: root.dim
|
||||
@@ -524,6 +530,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.fontFamily
|
||||
@@ -531,6 +538,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -531,6 +531,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.displays.length > 1 ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -559,6 +560,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroLabel
|
||||
textFormat: Text.PlainText
|
||||
text: {
|
||||
if (root.brightnessAvailable) {
|
||||
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
|
||||
@@ -602,6 +604,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: brightnessPercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -674,6 +677,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: textSizePx
|
||||
textFormat: Text.PlainText
|
||||
text: (textSizeSlider.dragging
|
||||
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
|
||||
: root.displayedTextPx()) + "px"
|
||||
@@ -747,6 +751,7 @@ Panel {
|
||||
// focused one.
|
||||
Text {
|
||||
id: scaleMonitor
|
||||
textFormat: Text.PlainText
|
||||
text: root.focusedMonitor
|
||||
// Only worth naming when more than one display is in play.
|
||||
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
|
||||
@@ -887,6 +892,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -897,6 +903,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: monitorRow.display.enabled ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -1090,6 +1090,7 @@ Panel {
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.icon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1170,6 +1171,7 @@ Panel {
|
||||
// rather than in a pill, which crowded the on/off switch.
|
||||
Text {
|
||||
id: heroSsid
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
|
||||
readonly property string title: {
|
||||
@@ -1189,6 +1191,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroMeta
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: {
|
||||
if (root.info.type === "wifi") {
|
||||
@@ -1711,6 +1714,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: networkIcon
|
||||
textFormat: Text.PlainText
|
||||
text: row.net ? root.wifiIconFor(row.net.signal) : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1732,6 +1736,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: lockIndicator
|
||||
textFormat: Text.PlainText
|
||||
visible: row.requiresCredentials || row.forgetVisible
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1779,6 +1784,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: row.net ? (row.net.ssid || "Hidden") : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1787,6 +1793,7 @@ Panel {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
// Signal strength is conveyed by the wifi-bars icon and the
|
||||
// right-edge glyph/buttons carry protection or forget affordances,
|
||||
// so the second line only carries action status (Connecting…,
|
||||
@@ -1893,6 +1900,7 @@ Panel {
|
||||
radius: Style.cornerRadius
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -1946,6 +1954,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1953,6 +1962,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -325,6 +325,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.batteryIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -356,6 +357,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroStatus
|
||||
textFormat: Text.PlainText
|
||||
text: root.heroStatusText.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -369,6 +371,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroPercent
|
||||
textFormat: Text.PlainText
|
||||
text: root.batteryInfo.percentage || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -517,6 +520,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -524,6 +528,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -498,6 +498,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||
width: parent.width
|
||||
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
|
||||
@@ -841,6 +842,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: accountRow.accountText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -933,6 +935,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: tailscale.osIcon(peer ? peer.OS : "")
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -946,6 +949,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: peerRow.peerName
|
||||
color: root.foreground
|
||||
@@ -955,6 +959,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
var parts = []
|
||||
@@ -1087,6 +1092,7 @@ Panel {
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: copyChoice.label
|
||||
color: root.foreground
|
||||
@@ -1134,6 +1140,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: exitNodeGlyph
|
||||
textFormat: Text.PlainText
|
||||
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "" : "")
|
||||
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -1154,6 +1161,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: exitNodeRow.peerName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -1224,6 +1232,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: regionRow.regionName
|
||||
color: root.foreground
|
||||
@@ -1234,6 +1243,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: regionRow.regionDetail
|
||||
visible: text !== ""
|
||||
|
||||
@@ -531,6 +531,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 5
|
||||
text: root.label || "—"
|
||||
@@ -547,6 +548,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: tempBig
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportTempNum || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -556,6 +558,7 @@ Panel {
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.current ? root.tempUnit : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -593,6 +596,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: (root.reportLocation || "").toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -643,6 +647,7 @@ Panel {
|
||||
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.savingLocation ? "" : "✕"
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -683,6 +688,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportFeels
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -700,6 +706,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportWind
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -717,6 +724,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportHumidity
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -752,12 +760,14 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.name
|
||||
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: modelData.description
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
@@ -817,6 +827,7 @@ Panel {
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.dayIcon(modelData)
|
||||
color: root.bar.foreground
|
||||
@@ -829,6 +840,7 @@ Panel {
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.dayName(modelData.date).toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -840,12 +852,14 @@ Panel {
|
||||
spacing: Style.space(6)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.bareTempForDay(modelData, "max")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.bareTempForDay(modelData, "min")
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -257,6 +257,7 @@ Item {
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: (root.ssid || "Wi-Fi").toUpperCase()
|
||||
color: root.onScrimDim
|
||||
font.family: root.fontFamily
|
||||
@@ -318,6 +319,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.error !== ""
|
||||
text: root.error
|
||||
color: root.onScrimUrgent
|
||||
@@ -340,6 +342,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showingQr && root.secured
|
||||
text: root.passwordError !== "" ? root.passwordError
|
||||
: root.passwordVisible ? root.password
|
||||
|
||||
@@ -332,6 +332,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -374,6 +375,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: justificationText
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(12)
|
||||
anchors.rightMargin: Style.space(12)
|
||||
|
||||
@@ -156,6 +156,7 @@ Item {
|
||||
anchors.leftMargin: card.contentLeftInset
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -32,6 +32,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.playIcon
|
||||
color: activePlayer && activePlayer.isPlaying ? root.bar.barForeground : Qt.darker(root.bar.barForeground, 1.5)
|
||||
@@ -53,6 +54,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
text: root.title + (root.artist ? " · " + root.artist : "")
|
||||
color: root.bar.barForeground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -148,6 +150,7 @@ BarWidget {
|
||||
width: parent.width - Style.space(74)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.title || "Nothing playing"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -158,6 +161,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.artist
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -168,6 +172,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -255,6 +260,7 @@ BarWidget {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.player && sourceRow.player.isPlaying ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -270,6 +276,7 @@ BarWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.sourceTitle
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -280,6 +287,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.sourceDetail
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -18,6 +18,151 @@ assertEqual(
|
||||
'notifications strip inline image tags'
|
||||
)
|
||||
|
||||
// The body renders as StyledText, which fetches <img src> over the network. The
|
||||
// invariant that matters is not a particular output string but that no tag Qt
|
||||
// would honour as an image survives, so assert that directly. Tags are bounded
|
||||
// the conservative way the stripper bounds them: a `<` opens a tag that runs to
|
||||
// the next `>`. Qt's own bound can be longer, since a `>` inside a quoted
|
||||
// attribute value does not close a tag there — which only ever splits one Qt
|
||||
// tag into several here, so a name this helper reads is a name Qt reads too.
|
||||
function survivingTagNames(text) {
|
||||
const names = []
|
||||
let i = 0
|
||||
while (i < text.length) {
|
||||
const open = text.indexOf('<', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexOf('>', open)
|
||||
const tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
|
||||
// Read the name the way Qt does, skipping anything that is not part of it.
|
||||
// Matching the separator with \s instead would give this helper the same
|
||||
// blind spot as the code it is checking — Qt skips U+0085 and \s does not —
|
||||
// and an assertion that shares the implementation's bug proves nothing.
|
||||
const name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
|
||||
if (name) names.push(name[1].toLowerCase())
|
||||
i = close === -1 ? text.length : close + 1
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Assert on styledBody, not sanitizeBody: styledBody is the string the card
|
||||
// binds to the StyledText, so it is the only one Qt ever parses. Checking the
|
||||
// sanitizer's output instead would pass a body whose surviving tag the newline
|
||||
// rewrite later splits open.
|
||||
function assertNoImageSurvives(body, description) {
|
||||
const out = notifications.styledBody(body, 'Slack', '')
|
||||
const names = survivingTagNames(out)
|
||||
assert(
|
||||
!names.includes('img'),
|
||||
description,
|
||||
`input: ${body}\noutput: ${out}\ntags: ${JSON.stringify(names)}`
|
||||
)
|
||||
}
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<img src="http://host/plain.png">',
|
||||
'notifications leave no image tag for a plain payload'
|
||||
)
|
||||
|
||||
// A payload spliced inside the literal "<img" prefix. Qt reads ONE malformed
|
||||
// tag named `im` here and renders nothing; a stripper that deleted the inner
|
||||
// match would close the halves up into a live <img> the input never had.
|
||||
assertNoImageSurvives(
|
||||
'<im<img src="http://host/decoy.png">g src="http://host/beacon.png">',
|
||||
'notifications leave no image tag when a payload is spliced inside <img'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<im<im<img src=a>g src=b>g src="http://host/deep.png">',
|
||||
'notifications leave no image tag for a doubly nested payload'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<img<img src="http://host/twin.png">',
|
||||
'notifications leave no image tag when the outer tag is itself named img'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'< img src="http://host/spaced.png">',
|
||||
'notifications leave no image tag when whitespace follows the angle bracket'
|
||||
)
|
||||
|
||||
// Qt skips the separator between `<` and the tag name with QChar::isSpace(),
|
||||
// which counts U+0085 NEL. JavaScript's \s does not. Reading the name with \s
|
||||
// finds none here, keeps the tag, and Qt then reads `img` and fetches it —
|
||||
// measured against Qt 6.11.2, where this exact body makes a StyledText Text
|
||||
// issue an outbound GET. Asserted on the whole output rather than through
|
||||
// assertNoImageSurvives so it holds even if that helper is ever loosened.
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<\u0085img src="http://host/nel.png">after', 'Slack', ''),
|
||||
'after',
|
||||
'notifications strip an image tag whose separator is U+0085, which Qt skips but \\s does not'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<\u0085img src="http://host/nel2.png">',
|
||||
'notifications leave no image tag when U+0085 follows the angle bracket'
|
||||
)
|
||||
|
||||
// The card rewrites newlines to <br/> for the StyledText, which puts tag syntax
|
||||
// inside a tag the stripper kept: `<x`, newline, `<img …>` is one tag named `x`
|
||||
// to both the stripper and Qt, and the rewrite splits it into `<x<br/>` and a
|
||||
// live image tag. Measured against Qt 6.11.2 — the rewritten form issues the GET
|
||||
// and the original does not — so the strip has to run after the rewrite, which
|
||||
// is what styledBody() does.
|
||||
assertNoImageSurvives(
|
||||
'<x\n<img src="http://host/split.png">',
|
||||
'notifications leave no image tag when a newline rewrite splits a kept tag'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<x\r\n<img src="http://host/split-crlf.png">',
|
||||
'notifications leave no image tag when a CRLF rewrite splits a kept tag'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.styledBody('<x\n<img src="http://host/split.png">', 'Slack', ''),
|
||||
'<x<br/>',
|
||||
'notifications drop the image half of a tag the newline rewrite splits'
|
||||
)
|
||||
|
||||
// The rewrite itself still happens, and body markup other than images survives it.
|
||||
assertEqual(
|
||||
notifications.styledBody('<b>bold</b>\nsecond line', 'Slack', ''),
|
||||
'<b>bold</b><br/>second line',
|
||||
'notifications keep body markup and the line break the card renders'
|
||||
)
|
||||
|
||||
// The order above is only worth anything if the card actually renders it, and no
|
||||
// JavaScript assertion can see a QML binding. Pin the binding itself: the rewrite
|
||||
// belongs in the logic module, where the strip runs after it.
|
||||
const cardQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/components/NotificationCard.qml'), 'utf8')
|
||||
assert(
|
||||
/readonly property string styledBody: NotificationLogic\.styledBody\(body, app, appIcon\)/.test(cardQml),
|
||||
'the notification card renders the body that was stripped after the newline rewrite'
|
||||
)
|
||||
assert(
|
||||
!/<br\/>/.test(cardQml),
|
||||
'the notification card does not rewrite newlines itself, which would leave tag syntax unchecked'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('trailing <img src="http://host/z.png"', 'Slack', ''),
|
||||
'trailing ',
|
||||
'notifications strip an unterminated image tag the renderer would close itself'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<IMG SRC="http://host/u.png">shout', 'Slack', ''),
|
||||
'shout',
|
||||
'notifications strip image tags regardless of case'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<b>bold</b> and <a href="http://host">link</a>', 'Slack', ''),
|
||||
'<b>bold</b> and <a href="http://host">link</a>',
|
||||
'notifications keep the body markup the body-markup capability advertises'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<a href="https://example.com">example.com</a> Message body', 'Chromium', ''),
|
||||
'Message body',
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
"""Report every QML Text that renders a non-literal value without a textFormat.
|
||||
|
||||
Usage: qml-text-format-scan.py ROOT (scans ROOT/shell, prints one line per
|
||||
violation, exits 1 on an unreadable tree). Lives in its own file rather than a
|
||||
heredoc so the test can run it over fixtures and prove it still fails when it
|
||||
should — a guard nothing can fail is a guard nobody should trust.
|
||||
|
||||
Two limits are deliberate, because a line scanner cannot close them. It reads
|
||||
each Text element's own declaration, so text assigned from somewhere else —
|
||||
`Binding { target: label; property: "text" }`, `PropertyChanges`, a
|
||||
`Component.onCompleted` assignment, a `property alias` onto a child's text —
|
||||
is invisible to it. And a regex literal containing a brace throws off the brace
|
||||
depth. Neither shape exists in this tree; both would need a QML parser.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BLOCK_COMMENT = re.compile(r'/\*.*?\*/|/\*.*\Z', re.S)
|
||||
|
||||
|
||||
def strip_block_comments(text):
|
||||
"""Blank out /* */ comments, keeping every newline so line numbers hold.
|
||||
|
||||
strip_noise() only knows `//`, so before this a block comment between a
|
||||
type name and its brace — `Text /* why */ {` — hid the element from
|
||||
OPEN_ELEMENT and from the unscannable-form check alike, and the block
|
||||
passed with no textFormat at all.
|
||||
"""
|
||||
out = []
|
||||
i = 0
|
||||
quote = None
|
||||
while i < len(text):
|
||||
c = text[i]
|
||||
if quote:
|
||||
if c == '\\':
|
||||
out.append(text[i:i + 2])
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c in '"\'':
|
||||
quote = c
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c == '/' and text.startswith('//', i):
|
||||
end = text.find('\n', i)
|
||||
if end == -1:
|
||||
break
|
||||
out.append(text[i:end])
|
||||
i = end
|
||||
continue
|
||||
if c == '/' and text.startswith('/*', i):
|
||||
end = text.find('*/', i + 2)
|
||||
end = len(text) if end == -1 else end + 2
|
||||
out.append(''.join(ch if ch == '\n' else ' ' for ch in text[i:end]))
|
||||
i = end
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
# A Text under a namespaced import — `import QtQuick as QQ` then `QQ.Text` — is
|
||||
# the same element and was skipped, because the name compared unequal to `Text`.
|
||||
TEXT_NAME = r'(?:[A-Za-z_][A-Za-z0-9_]*\.)?Text'
|
||||
|
||||
OPEN_ELEMENT = re.compile(r'(?:^|[:\s])([A-Z][A-Za-z0-9_.]*)\s*\{\s*$')
|
||||
INLINE_COMPONENT = re.compile(r'^\s*component\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*' + TEXT_NAME + r'\s*\{\s*$')
|
||||
INLINE_COMPONENT_ONELINE = re.compile(r'^\s*component\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*' + TEXT_NAME + r'\s*\{')
|
||||
PROP = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_.]*)\s*:')
|
||||
STRING_LITERAL = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
||||
PROPERTY_DECL = re.compile(r'^\s*(?:readonly\s+)?property\b')
|
||||
# A binding that runs onto the next line: this line ends on an operator, or the
|
||||
# next line opens with one.
|
||||
TRAILING_OPERATOR = re.compile(r'(?:&&|\|\||[?:+\-*/,(\[=&|])$')
|
||||
LEADING_OPERATOR = re.compile(r'^\s*(?:&&|\|\||[?:+\-*/,)\]&|.])')
|
||||
|
||||
|
||||
def strip_noise(line, keep_strings=False):
|
||||
out = []
|
||||
i = 0
|
||||
quote = None
|
||||
while i < len(line):
|
||||
c = line[i]
|
||||
if quote:
|
||||
if keep_strings:
|
||||
out.append(c)
|
||||
if c == '\\':
|
||||
if keep_strings and i + 1 < len(line):
|
||||
out.append(line[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
if not keep_strings:
|
||||
out.append('S')
|
||||
i += 1
|
||||
continue
|
||||
if c in '"\'':
|
||||
quote = c
|
||||
if keep_strings:
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c == '/' and i + 1 < len(line) and line[i + 1] == '/':
|
||||
break
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def is_pure_literal(expr):
|
||||
residue = STRING_LITERAL.sub('', expr)
|
||||
residue = re.sub(r'[\s+]', '', residue)
|
||||
return residue == '' and STRING_LITERAL.search(expr) is not None
|
||||
|
||||
|
||||
def binding_expression(lines, start):
|
||||
"""The whole right-hand side of the binding beginning on line `start`.
|
||||
|
||||
The literal exemption has to be judged on the complete expression. Reading
|
||||
only the physical `text:` line would exempt `text: "prefix"` while
|
||||
`+ externalValue` sits underneath, letting a dynamic AutoText binding
|
||||
through. Reading a wrapped concatenation of literals as dynamic would be
|
||||
the opposite error, so follow the expression to its end either way.
|
||||
"""
|
||||
parts = []
|
||||
parens = brackets = 0
|
||||
i = start
|
||||
while i < len(lines):
|
||||
parts.append(strip_noise(lines[i], keep_strings=True))
|
||||
counted = strip_noise(lines[i])
|
||||
parens += counted.count('(') - counted.count(')')
|
||||
brackets += counted.count('[') - counted.count(']')
|
||||
# Look past blank and comment-only lines for the continuation. A
|
||||
# comment or a blank line dropped into a wrapped expression does not
|
||||
# end it, and stopping there would read `text: "prefix"` as the whole
|
||||
# binding and exempt it as a literal while `+ externalValue` waits
|
||||
# below — the exact misreading this function exists to prevent.
|
||||
following = ''
|
||||
for ahead in range(i + 1, len(lines)):
|
||||
candidate = strip_noise(lines[ahead])
|
||||
if candidate.strip():
|
||||
following = candidate
|
||||
break
|
||||
continues = (parens > 0 or brackets > 0
|
||||
or TRAILING_OPERATOR.search(counted.rstrip())
|
||||
or LEADING_OPERATOR.match(following))
|
||||
if not continues:
|
||||
break
|
||||
i += 1
|
||||
|
||||
chunk = ' '.join(parts)
|
||||
return chunk.split(':', 1)[1] if ':' in chunk else chunk
|
||||
|
||||
|
||||
def exempt_as_literal(lines, tline):
|
||||
"""True when the binding is only string literals, however many lines."""
|
||||
return is_pure_literal(binding_expression(lines, tline))
|
||||
|
||||
|
||||
def blocks(lines):
|
||||
stack = []
|
||||
done = []
|
||||
depth = 0
|
||||
for idx, raw in enumerate(lines):
|
||||
code = strip_noise(raw)
|
||||
opened = OPEN_ELEMENT.search(code)
|
||||
prop = PROP.match(code)
|
||||
if (prop and stack and stack[-1]['depth'] == depth
|
||||
and not opened and not PROPERTY_DECL.match(code)):
|
||||
stack[-1]['props'].setdefault(prop.group(1), idx)
|
||||
n_open = code.count('{')
|
||||
n_close = code.count('}')
|
||||
depth += n_open - n_close
|
||||
if opened and n_open > 0:
|
||||
# OPEN_ELEMENT anchors at the end of the line, so the element it
|
||||
# matched is the innermost one opened here and its depth is the
|
||||
# depth after every brace on the line.
|
||||
stack.append({'name': opened.group(1), 'depth': depth,
|
||||
'props': {}, 'start': idx})
|
||||
while stack and depth < stack[-1]['depth']:
|
||||
done.append(stack.pop())
|
||||
done.extend(stack)
|
||||
return done
|
||||
|
||||
|
||||
INLINE_TEXT = re.compile(r'(?:^|[:\s])' + TEXT_NAME + r'\s*\{([^{}]*)\}')
|
||||
INLINE_BINDING = re.compile(r'\btext\s*:\s*(.*?)\s*(?:;|$)')
|
||||
# As a property of this block, not as a substring: `visible: root.textFormatEnabled`
|
||||
# used to read as a declaration and exempt the element.
|
||||
INLINE_TEXT_FORMAT = re.compile(r'(?:^|[;{\s])textFormat\s*:')
|
||||
|
||||
|
||||
def inline_violations(lines, rel):
|
||||
"""Whole Text blocks written on one line.
|
||||
|
||||
OPEN_ELEMENT anchors at the end of the line, so the brace scanner never
|
||||
sees these. A Repeater delegate is a plausible place for one.
|
||||
"""
|
||||
out = []
|
||||
for idx, raw in enumerate(lines):
|
||||
code = strip_noise(raw, keep_strings=True)
|
||||
for match in INLINE_TEXT.finditer(code):
|
||||
body = match.group(1)
|
||||
if INLINE_TEXT_FORMAT.search(body):
|
||||
continue
|
||||
# A component root written on one line needs the default whether or
|
||||
# not this line binds `text`, for the same reason the block form
|
||||
# does: every caller supplies the binding.
|
||||
if INLINE_COMPONENT_ONELINE.match(code):
|
||||
out.append(f'{rel}:{idx + 1}: inline component root Text declares no textFormat')
|
||||
continue
|
||||
binding = INLINE_BINDING.search(body)
|
||||
if not binding or is_pure_literal(binding.group(1)):
|
||||
continue
|
||||
out.append(f'{rel}:{idx + 1}: inline Text block without textFormat')
|
||||
return out
|
||||
|
||||
|
||||
# `Text { text: someValue` with the block carrying on below is valid QML and is
|
||||
# invisible to both scanners: OPEN_ELEMENT anchors its `{` at the end of the
|
||||
# line so the brace tracker never opens the block, and INLINE_TEXT needs the
|
||||
# closing brace on the same line. A dynamic AutoText binding written that way
|
||||
# passes this file in silence, which is the one failure a test like this must
|
||||
# not have.
|
||||
#
|
||||
# Rather than teach a line scanner to parse QML, require the two forms it can
|
||||
# read: the whole block on one line, or nothing after the opening brace. Every
|
||||
# Text in this tree is already written that way, so keeping to it costs nothing.
|
||||
UNSCANNABLE_TEXT = re.compile(r'(?:^|[:\s])' + TEXT_NAME + r'\s*\{\s*\S')
|
||||
BARE_TEXT_OPENER = re.compile(r'(?:^|[:\s])' + TEXT_NAME + r'\s*$')
|
||||
|
||||
UNSCANNABLE = ('Text block written in a form this scanner cannot read; put the '
|
||||
'opening brace last on the line, or write the whole block on '
|
||||
'one line with no nested braces')
|
||||
|
||||
|
||||
COMPONENT_OPENER = re.compile(r'^\s*component\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*$')
|
||||
|
||||
|
||||
def opens_component(lines, start):
|
||||
"""True when the Text block at `start` is a component root declared above it."""
|
||||
for back in range(start - 1, -1, -1):
|
||||
code = strip_noise(lines[back]).strip()
|
||||
if not code:
|
||||
continue
|
||||
return bool(COMPONENT_OPENER.match(lines[back]))
|
||||
return False
|
||||
|
||||
|
||||
def unscannable_violations(lines, rel):
|
||||
out = []
|
||||
for idx, raw in enumerate(lines):
|
||||
code = strip_noise(raw)
|
||||
|
||||
# `Text` with its brace on the next line. OPEN_ELEMENT needs both on
|
||||
# one line, so the block is never opened and everything in it is
|
||||
# attributed to the enclosing element instead.
|
||||
if BARE_TEXT_OPENER.search(code):
|
||||
following = ''
|
||||
for ahead in range(idx + 1, len(lines)):
|
||||
candidate = strip_noise(lines[ahead]).strip()
|
||||
if candidate:
|
||||
following = candidate
|
||||
break
|
||||
if following.startswith('{'):
|
||||
out.append(f'{rel}:{idx + 1}: {UNSCANNABLE}')
|
||||
continue
|
||||
|
||||
for match in UNSCANNABLE_TEXT.finditer(code):
|
||||
# A complete one-line block with no nested braces is fine —
|
||||
# inline_violations reads those. Count rather than looking for a
|
||||
# `}`, because `Text { text: ({ a: external }).a }` closes on this
|
||||
# line yet INLINE_TEXT's brace-free body pattern cannot match it,
|
||||
# so treating any `}` as "handled elsewhere" would drop it.
|
||||
rest = code[match.end() - 1:]
|
||||
depth = 1
|
||||
closed = False
|
||||
for char in rest:
|
||||
if char == '{':
|
||||
depth += 1
|
||||
elif char == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
closed = True
|
||||
break
|
||||
if closed and '{' not in rest:
|
||||
continue
|
||||
out.append(f'{rel}:{idx + 1}: {UNSCANNABLE}')
|
||||
return out
|
||||
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
found = []
|
||||
scanned = 0
|
||||
|
||||
|
||||
def unreadable(error):
|
||||
# rglob() swallows a directory it cannot enter, so a shell/ subtree with no
|
||||
# read permission scanned as though it were empty and the run reported
|
||||
# success. Same failure as an empty tree, and it fails the same way.
|
||||
raise SystemExit(f'cannot read {error.filename}: {error.strerror}')
|
||||
|
||||
|
||||
qml = []
|
||||
for dirpath, dirnames, filenames in os.walk(root / 'shell', onerror=unreadable):
|
||||
dirnames.sort()
|
||||
qml.extend(Path(dirpath) / name for name in filenames if name.endswith('.qml'))
|
||||
|
||||
for path in sorted(qml):
|
||||
scanned += 1
|
||||
lines = strip_block_comments(path.read_text()).splitlines()
|
||||
rel = path.relative_to(root)
|
||||
found.extend(inline_violations(lines, rel))
|
||||
found.extend(unscannable_violations(lines, rel))
|
||||
|
||||
for b in blocks(lines):
|
||||
if b['name'].split('.')[-1] != 'Text' or 'textFormat' in b['props']:
|
||||
continue
|
||||
|
||||
# Read the block's own properties. A nested child declaring textFormat
|
||||
# says nothing about its parent, so `Text { Text { textFormat: ... } }`
|
||||
# must still report the outer element.
|
||||
# The root element of a component takes its binding from callers, so it
|
||||
# needs the default whether or not this file binds `text`. Require both
|
||||
# depth 1 and column 0: the scanner attributes one element per line, so
|
||||
# a `Row { Text {` line would report depth 1 for a nested block, and
|
||||
# falling through to the binding check below is the safe reading.
|
||||
# Indentation is not what makes it a root; depth 1 is. A `Row { Text {`
|
||||
# line still reads as `Row` here, so leading whitespace can be ignored
|
||||
# without letting a nested block be mistaken for the file's root.
|
||||
if b['depth'] == 1 and lines[b['start']].lstrip().startswith('Text'):
|
||||
found.append(f'{rel}:{b["start"] + 1}: root Text element declares no textFormat')
|
||||
continue
|
||||
|
||||
# A QML inline component is a root for the same reason, and the rule
|
||||
# above cannot see one: `component InfoValue: Text {` sits inside
|
||||
# another element, so its depth is not 1 and its line does not start
|
||||
# with `Text`. Its `text` comes from every caller, so the file it lives
|
||||
# in never binds it and the binding check below lets it through in
|
||||
# silence. Only one file-level root Text exists in this tree, so
|
||||
# without this the root rule is very nearly dead code.
|
||||
# `component Info:` may also put its `Text {` on the following line,
|
||||
# which INLINE_COMPONENT cannot match and which then reads as an
|
||||
# ordinary nested block with no binding of its own — a caller's dynamic
|
||||
# text passing in silence.
|
||||
if INLINE_COMPONENT.match(lines[b['start']]) or opens_component(lines, b['start']):
|
||||
found.append(f'{rel}:{b["start"] + 1}: inline component root Text declares no textFormat')
|
||||
continue
|
||||
|
||||
if 'text' not in b['props']:
|
||||
continue
|
||||
tline = b['props']['text']
|
||||
if exempt_as_literal(lines, tline):
|
||||
continue
|
||||
found.append(f'{rel}:{tline + 1}: text binding without textFormat')
|
||||
|
||||
# A scan that read nothing reports nothing, and an all-clear from a run that
|
||||
# never opened a file is the one result this test must never give. Only a
|
||||
# checkout with no shell/ QML at all reaches this.
|
||||
if scanned == 0:
|
||||
raise SystemExit('no .qml files found under shell/; the scan read nothing')
|
||||
|
||||
for line in found:
|
||||
print(line)
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A QML Text element with no textFormat uses Text.AutoText. Qt then runs
|
||||
# mightBeRichText() over the string and promotes it to Text.RichText when it
|
||||
# looks like markup, and RichText fetches <img src="http://..."> through
|
||||
# QQuickPixmap. Any string that reaches such an element from outside the shell
|
||||
# — a notification summary, an MPRIS track title, a window title, an SSID, a
|
||||
# Bluetooth device name, clipboard content, a weather API response — can
|
||||
# therefore make the shell issue an unauthenticated outbound GET with no user
|
||||
# interaction.
|
||||
#
|
||||
# The promotion needs only that the attacker contribute the first `<` in the
|
||||
# string, on the first line. A fixed label in front of the value does not
|
||||
# protect it, and neither does .toUpperCase(), because the parser lowercases
|
||||
# the tag before looking it up.
|
||||
#
|
||||
# So require an explicit textFormat on every Text whose text: binding is not a
|
||||
# bare string literal. A literal carries no external data, so AutoText has
|
||||
# nothing to promote; this test is what catches the edit that later turns such
|
||||
# a literal into an expression.
|
||||
#
|
||||
# The scan itself lives in qml-text-format-scan.py. It is run twice: over the
|
||||
# real tree, and over the fixtures below, which are the forms that have already
|
||||
# slipped past it once. A guard nothing can fail is a guard nobody should trust,
|
||||
# and every one of those fixtures passed silently before it was written down.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
require_command python3
|
||||
|
||||
SCAN="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/qml-text-format-scan.py"
|
||||
|
||||
violations=$(python3 "$SCAN" "$ROOT")
|
||||
|
||||
if [[ -n $violations ]]; then
|
||||
count=$(printf '%s\n' "$violations" | wc -l)
|
||||
fail "every Text with a dynamic text binding declares textFormat" \
|
||||
"$violations
|
||||
|
||||
$count Text element(s) rely on Text.AutoText for a non-literal binding.
|
||||
Add an explicit textFormat. Text.PlainText is right for anything that renders
|
||||
data from outside the shell; use Text.StyledText only where markup is a
|
||||
deliberate, documented feature, and strip <img> before it reaches the renderer."
|
||||
fi
|
||||
|
||||
pass "every Text with a dynamic text binding declares textFormat"
|
||||
|
||||
# The scanner's own tests. Each fixture is a Text that renders external data
|
||||
# with no textFormat, written in a form that once passed. `caught` asserts the
|
||||
# scan reports something; `clean` asserts it does not, so the fixtures prove the
|
||||
# scanner can fail rather than that it fails at everything.
|
||||
fixture_root=$(mktemp -d)
|
||||
trap 'chmod -R u+rwX "$fixture_root" 2>/dev/null; rm -rf "$fixture_root"' EXIT
|
||||
|
||||
function scan_fixture {
|
||||
local name=$1
|
||||
local dir="$fixture_root/$name"
|
||||
mkdir -p "$dir/shell/Ui"
|
||||
cat > "$dir/shell/Ui/Fixture.qml"
|
||||
python3 "$SCAN" "$dir" 2>&1
|
||||
}
|
||||
|
||||
function caught {
|
||||
local name=$1 description=$2 output
|
||||
output=$(scan_fixture "$name" || true)
|
||||
if [[ -z $output ]]; then
|
||||
fail "$description" "the scan reported nothing for fixture $name"
|
||||
fi
|
||||
pass "$description"
|
||||
}
|
||||
|
||||
function clean {
|
||||
local name=$1 description=$2 output
|
||||
output=$(scan_fixture "$name" || true)
|
||||
if [[ -n $output ]]; then
|
||||
fail "$description" "the scan reported: $output"
|
||||
fi
|
||||
pass "$description"
|
||||
}
|
||||
|
||||
caught plain "the scan reports a plain dynamic binding with no textFormat" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text {
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
clean literal "the scan leaves a string literal alone" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
Text {
|
||||
text: "a literal"
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
clean declared "the scan leaves a declared textFormat alone" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# strip_noise() knew `//` and not `/* */`, so a block comment between the type
|
||||
# name and its brace hid the whole element from every rule.
|
||||
caught block-comment "the scan reads a Text whose brace a block comment hides" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text /* explanation */ {
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
caught block-comment-multiline "the scan reads past a block comment spanning lines" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
/*
|
||||
* Text { text: "not this one" }
|
||||
*/
|
||||
Text {
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# `import QtQuick as QQ` makes the element `QQ.Text`, which compared unequal to
|
||||
# `Text` and was skipped outright.
|
||||
caught namespaced "the scan reads a Text reached through a namespaced import" <<'QML'
|
||||
import QtQuick as QQ
|
||||
QQ.Item {
|
||||
property string external: "x"
|
||||
QQ.Text {
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# textFormat was matched as a substring, so any property whose name merely
|
||||
# started that way exempted the element.
|
||||
caught namespaced-inline "the scan reads a one-line namespaced Text block" <<'QML'
|
||||
import QtQuick as QQ
|
||||
QQ.Item {
|
||||
property string external: "x"
|
||||
QQ.Text { text: external }
|
||||
}
|
||||
QML
|
||||
|
||||
caught namespaced-unscannable "the scan rejects an unreadable namespaced Text block" <<'QML'
|
||||
import QtQuick as QQ
|
||||
QQ.Item {
|
||||
property string external: "x"
|
||||
QQ.Text { text: external
|
||||
color: "red"
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
caught textformat-substring "the scan does not accept a lookalike property as textFormat" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
property bool textFormatEnabled: true
|
||||
Text { text: external; visible: textFormatEnabled }
|
||||
}
|
||||
QML
|
||||
|
||||
# A component root takes its text from every caller, so the file it lives in
|
||||
# never binds it. The one-line form was covered; this one was not.
|
||||
caught component-next-line "the scan reads a component root whose Text sits on the next line" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
component Info:
|
||||
Text {
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
caught component-one-line "the scan reads a component root written on one line" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
component Info: Text { color: "red" }
|
||||
}
|
||||
QML
|
||||
|
||||
# Forms the scanner cannot read are reported rather than passed, which is the
|
||||
# whole reason it can be a line scanner at all.
|
||||
caught brace-next-line "the scan rejects a Text whose opening brace is on the next line" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text
|
||||
{
|
||||
text: external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
caught trailing-binding "the scan rejects a Text with a binding after the opening brace" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text { text: external
|
||||
color: "red"
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# A wrapped binding is judged whole: a literal first line says nothing about
|
||||
# what is concatenated onto it below.
|
||||
caught wrapped-binding "the scan follows a wrapped binding past its literal first line" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
property string external: "x"
|
||||
Text {
|
||||
text: "prefix"
|
||||
+ external
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
clean wrapped-literals "the scan leaves a wrapped concatenation of literals alone" <<'QML'
|
||||
import QtQuick
|
||||
Item {
|
||||
Text {
|
||||
text: "one"
|
||||
+ "two"
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# A nested child's textFormat says nothing about its parent.
|
||||
caught nested-child "the scan does not let a nested child's textFormat cover its parent" <<'QML'
|
||||
import QtQuick
|
||||
Text {
|
||||
text: external.value
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: "literal"
|
||||
}
|
||||
}
|
||||
QML
|
||||
|
||||
# A scan that reads less than the tree holds must not report success. Both of
|
||||
# these once did.
|
||||
empty_root=$(mktemp -d)
|
||||
mkdir -p "$empty_root/shell"
|
||||
if python3 "$SCAN" "$empty_root" > /dev/null 2>&1; then
|
||||
rm -rf "$empty_root"
|
||||
fail "the scan fails when it reads no files" "an empty shell/ tree exited 0"
|
||||
fi
|
||||
rm -rf "$empty_root"
|
||||
pass "the scan fails when it reads no files"
|
||||
|
||||
blind_root="$fixture_root/blind"
|
||||
mkdir -p "$blind_root/shell/Ui/locked"
|
||||
printf 'import QtQuick\nItem {\n Text {\n textFormat: Text.PlainText\n text: "ok"\n }\n}\n' > "$blind_root/shell/Ui/Good.qml"
|
||||
printf 'import QtQuick\nItem {\n property string external: "x"\n Text {\n text: external\n }\n}\n' > "$blind_root/shell/Ui/locked/Bad.qml"
|
||||
chmod 000 "$blind_root/shell/Ui/locked"
|
||||
if python3 "$SCAN" "$blind_root" > /dev/null 2>&1; then
|
||||
chmod 755 "$blind_root/shell/Ui/locked"
|
||||
fail "the scan fails when a directory hides files from it" "an unreadable subdirectory exited 0"
|
||||
fi
|
||||
chmod 755 "$blind_root/shell/Ui/locked"
|
||||
pass "the scan fails when a directory hides files from it"
|
||||
Reference in New Issue
Block a user