mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-18 11:19:30 +08:00
* rdar://108152217 Start using Rope for attribute run storage - Replace `Array<_InternalRun>` with `Rope<_InternalRun>`. - Fix semantics of AttributeRunBoundaries.character. It no longer pretends that attributes can be tied to grapheme clusters — that never actually worked properly, and it cannot be supported without breaking the intended use case. - Remove cache of latest run position; log(n) might be fast enough that we don’t need to worry about that. - Switch AttributedString.Guts members to take/return BigString.Index values, not AttributedString indices. This cuts down on constant back-and-forth conversions. - Discard unused Guts members; update remaining mutation methods to follow Swift naming conventions. - Stop using the `public extension` language misfeature. * rdar://108152217 Apply notes from code review AttributedString.CharacterView and AttributedString.UnicodeScalarView did not use the right semantics in the case when the underlying text was not touched. It’s okay in that case to not mutate text storage, but we still want to override the attributes within the subrange as if the client actually did replace text — as in, we want to use precisely the same attribute storage for the range on both paths. Also, make sure we correctly enforce attribute constraints in the `!hasStringChanges` case, in both views. * rdar://108152217 Fix logic error in AttributedString.Runs.subscript (Discovered during code review.) * rdar://108152217 Clean up CharacterView/UnicodeScalarView mutations & update failing test * rdar://108152217 Apply review notes
505 lines
20 KiB
Swift
505 lines
20 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2020-2023 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
@_implementationOnly @_spi(Unstable) import CollectionsInternal
|
|
#else
|
|
package import _RopeModule
|
|
#endif
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString {
|
|
public struct Runs: Sendable {
|
|
internal typealias _InternalRun = AttributedString._InternalRun
|
|
internal typealias _AttributeStorage = AttributedString._AttributeStorage
|
|
internal typealias _InternalRuns = AttributedString._InternalRuns
|
|
internal typealias AttributeRunBoundaries = AttributedString.AttributeRunBoundaries
|
|
|
|
internal var _guts: Guts
|
|
internal var _bounds: Range<Index>
|
|
internal var _strBounds: Range<BigString.Index>
|
|
|
|
internal init(_ guts: Guts, in bounds: Range<BigString.Index>) {
|
|
_guts = guts
|
|
|
|
let stringLowerBound = _guts.string.unicodeScalars.index(roundingDown: bounds.lowerBound)
|
|
let stringUpperBound = _guts.string.unicodeScalars.index(roundingDown: bounds.upperBound)
|
|
_strBounds = stringLowerBound ..< stringUpperBound
|
|
|
|
let lower = _guts.findRun(at: _strBounds.lowerBound)
|
|
let start = Index(_runIndex: lower.runIndex, stringIndex: lower.start)
|
|
|
|
let end: Index
|
|
|
|
if _strBounds.upperBound == _guts.string.endIndex {
|
|
end = Index(
|
|
_runOffset: _guts.runs.count,
|
|
runIndex: _guts.runs.endIndex.base,
|
|
stringIndex: _strBounds.upperBound)
|
|
} else if _strBounds.upperBound == _guts.string.startIndex {
|
|
assert(stringLowerBound == stringUpperBound)
|
|
end = start
|
|
} else {
|
|
let last = _guts.runs.index(atUTF8Offset: _strBounds.upperBound.utf8Offset - 1).index
|
|
let next = _guts.runs.index(after: last)
|
|
|
|
let stringEnd = _guts.string.utf8.index(
|
|
_strBounds.upperBound,
|
|
offsetBy: next.utf8Offset - _strBounds.upperBound.utf8Offset)
|
|
end = Index(_runIndex: next, stringIndex: stringEnd)
|
|
}
|
|
assert(start._runIndex != nil && start._stringIndex != nil)
|
|
assert(end._runIndex != nil && end._stringIndex != nil)
|
|
assert(start._stringIndex!.utf8Offset <= _strBounds.lowerBound.utf8Offset)
|
|
assert(end._stringIndex!.utf8Offset >= _strBounds.upperBound.utf8Offset)
|
|
self._bounds = start ..< end
|
|
}
|
|
}
|
|
|
|
public var runs: Runs {
|
|
Runs(_guts, in: _guts.string.startIndex ..< _guts.string.endIndex)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs: Equatable {
|
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
|
// Note: Unlike AttributedString itself, this is comparing run lengths without normalizing
|
|
// the underlying characters.
|
|
//
|
|
// I.e., the runs of two equal attribute strings may or may not compare equal.
|
|
|
|
// Shortcut: compare overall UTF-8 counts.
|
|
let leftUTF8Count = lhs._strBounds._utf8OffsetRange.count
|
|
let rightUTF8Count = rhs._strBounds._utf8OffsetRange.count
|
|
guard leftUTF8Count == rightUTF8Count else { return false }
|
|
|
|
// Shortcut: compare run counts.
|
|
let leftRunCount = lhs._bounds.upperBound._runOffset - lhs._bounds.lowerBound._runOffset
|
|
let rightRunCount = rhs._bounds.upperBound._runOffset - rhs._bounds.lowerBound._runOffset
|
|
guard leftRunCount == rightRunCount else { return false }
|
|
|
|
return lhs.elementsEqual(rhs)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs: CustomStringConvertible {
|
|
public var description: String {
|
|
_guts.description(in: _strBounds)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs {
|
|
public struct Index: Sendable {
|
|
/// The offset of this run from the start of the attributed string.
|
|
/// This is always set to a valid value.
|
|
internal var _runOffset: Int
|
|
|
|
/// The underlying index in the rope.
|
|
///
|
|
/// This may be nil if the index was advanced without going through the Collection APIs;
|
|
/// in that case, the index can be restored using the offset, although
|
|
/// at a log(count) cost.
|
|
internal var _runIndex: _InternalRuns.Storage.Index?
|
|
|
|
/// The position in string storage corresponding to the start of this run.
|
|
/// This may be outside of the bounds of the `Runs` if this is addressing the first run.
|
|
/// (I.e., this is the unsliced, global start of the run.)
|
|
///
|
|
/// This may be nil if the index was advanced without going through the Collection APIs;
|
|
/// in that case, the index can be restored using the offset, although
|
|
/// at a log(count) cost.
|
|
internal var _stringIndex: BigString.Index?
|
|
|
|
internal init(_runOffset: Int) {
|
|
self._runOffset = _runOffset
|
|
self._runIndex = nil
|
|
self._stringIndex = nil
|
|
}
|
|
|
|
internal init(_runOffset: Int, runIndex: _InternalRuns.Storage.Index, stringIndex: BigString.Index) {
|
|
self._runOffset = _runOffset
|
|
self._runIndex = runIndex
|
|
self._stringIndex = stringIndex
|
|
}
|
|
|
|
internal init(_runIndex: _InternalRuns.Index, stringIndex: BigString.Index) {
|
|
self._runOffset = _runIndex.offset
|
|
self._runIndex = _runIndex.base
|
|
self._stringIndex = stringIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs.Index: Comparable {
|
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
|
lhs._runOffset == rhs._runOffset
|
|
}
|
|
|
|
public static func < (lhs: Self, rhs: Self) -> Bool {
|
|
lhs._runOffset < rhs._runOffset
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs.Index: Strideable {
|
|
// FIXME: `Index` conforming to `Strideable` was an unfortunate choice.
|
|
// It means we lose direct rope indices whenever someone advances a standalone index,
|
|
// slowing down subsequent access.
|
|
|
|
public func distance(to other: Self) -> Int {
|
|
other._runOffset - self._runOffset
|
|
}
|
|
|
|
public func advanced(by n: Int) -> Self {
|
|
Self(_runOffset: self._runOffset + n)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension Range<AttributedString.Runs.Index> {
|
|
var _runOffsetRange: Range<Int> {
|
|
Range<Int>(uncheckedBounds: (lowerBound._runOffset, upperBound._runOffset))
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs: BidirectionalCollection {
|
|
public typealias Element = Run
|
|
|
|
internal func _resolveRun(_ i: Index) -> _InternalRuns.Index {
|
|
precondition(i >= _bounds.lowerBound && i <= _bounds.upperBound, "Index out of bounds")
|
|
guard let ri = i._runIndex, _guts.runs._rope.isValid(ri) else {
|
|
return _guts.runs.index(atRunOffset: i._runOffset)
|
|
}
|
|
let utf8Offset = (
|
|
i._stringIndex.map { $0.utf8Offset }
|
|
?? _guts.runs._rope.offset(of: ri, in: _InternalRuns.UTF8Metric()))
|
|
return _InternalRuns.Index(ri, offset: i._runOffset, utf8Offset: utf8Offset)
|
|
}
|
|
|
|
internal func _resolve(_ i: Index) -> (runIndex: _InternalRuns.Index, start: BigString.Index) {
|
|
let runIndex = _resolveRun(i)
|
|
var start: BigString.Index
|
|
if let si = i._stringIndex, si.utf8Offset == runIndex.utf8Offset {
|
|
// Don't trust that the string index is still valid. Let BigString resolve it.
|
|
start = _guts.string.utf8.index(roundingDown: si)
|
|
} else {
|
|
start = _guts.utf8Index(at: runIndex.utf8Offset)
|
|
}
|
|
return (runIndex, start)
|
|
}
|
|
|
|
public var startIndex: Index {
|
|
_bounds.lowerBound
|
|
}
|
|
|
|
public var endIndex: Index {
|
|
_bounds.upperBound
|
|
}
|
|
|
|
public func index(after i: Index) -> Index {
|
|
precondition(i >= _bounds.lowerBound, "AttributedString.Runs index out of bounds")
|
|
precondition(i < _bounds.upperBound, "Can't advance AttributedString.Runs index beyond end")
|
|
let next = _guts.runs.index(after: _resolveRun(i))
|
|
let stringIndex = (
|
|
i._stringIndex.map { _guts.string.utf8.index($0, offsetBy: next.utf8Offset - $0.utf8Offset) }
|
|
?? _guts.utf8Index(at: next.utf8Offset))
|
|
return Index(_runIndex: next, stringIndex: stringIndex)
|
|
}
|
|
|
|
public func index(before i: Index) -> Index {
|
|
precondition(i > _bounds.lowerBound, "Can't step AttributedString.Runs index below start")
|
|
let prev = _guts.runs.index(before: _resolveRun(i))
|
|
let stringIndex = (
|
|
i._stringIndex.map { _guts.string.utf8.index($0, offsetBy: prev.utf8Offset - $0.utf8Offset) }
|
|
?? _guts.utf8Index(at: prev.utf8Offset))
|
|
return Index(_runIndex: prev, stringIndex: stringIndex)
|
|
}
|
|
|
|
@_alwaysEmitIntoClient
|
|
public func distance(from start: Index, to end: Index) -> Int {
|
|
start.distance(to: end)
|
|
}
|
|
|
|
@_alwaysEmitIntoClient
|
|
public func index(_ i: Index, offsetBy distance: Int) -> Index {
|
|
if #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) {
|
|
return _index(i, offsetBy: distance)
|
|
}
|
|
return i.advanced(by: distance)
|
|
}
|
|
|
|
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
|
|
@usableFromInline
|
|
internal func _index(_ index: Index, offsetBy distance: Int) -> Index {
|
|
let i = _guts.runs.index(_resolveRun(index), offsetBy: distance)
|
|
// Note: bounds checking of result is delayed until subscript.
|
|
let stringIndex = (
|
|
index._stringIndex.map { _guts.string.utf8.index($0, offsetBy: i.utf8Offset - $0.utf8Offset) }
|
|
?? _guts.utf8Index(at: i.utf8Offset))
|
|
return Index(_runIndex: i, stringIndex: stringIndex)
|
|
}
|
|
|
|
@_alwaysEmitIntoClient
|
|
public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? {
|
|
// This is the stdlib's default implementation for RandomAccessCollection types.
|
|
// (It's _far_ more efficient than the O(n) algorithm that used to apply here by default,
|
|
// in both the original and the tree-based representation.)
|
|
let l = self.distance(from: i, to: limit)
|
|
if distance > 0 ? l >= 0 && l < distance : l <= 0 && distance < l {
|
|
return nil
|
|
}
|
|
return index(i, offsetBy: distance)
|
|
}
|
|
|
|
public subscript(position: Index) -> Run {
|
|
precondition(_bounds.contains(position), "AttributedString.Runs index is out of bounds")
|
|
return self[_unchecked: _resolve(position)]
|
|
}
|
|
|
|
public subscript(position: AttributedString.Index) -> Run {
|
|
precondition(
|
|
_strBounds.contains(position._value),
|
|
"AttributedString index is out of bounds")
|
|
let r = _guts.findRun(at: position._value)
|
|
return self[_unchecked: r]
|
|
}
|
|
|
|
internal subscript(_unchecked i: (runIndex: _InternalRuns.Index, start: BigString.Index)) -> Run {
|
|
let run = _guts.runs[i.runIndex]
|
|
// Clamp the run into the bounds of self, using relative calculations.
|
|
let lowerBound = Swift.max(i.start, _strBounds.lowerBound)
|
|
let upperUTF8 = Swift.min(i.start.utf8Offset + run.length, _strBounds.upperBound.utf8Offset)
|
|
let upperBound = _guts.string.utf8.index(lowerBound, offsetBy: upperUTF8 - lowerBound.utf8Offset)
|
|
return Run(_attributes: run.attributes, lowerBound ..< upperBound, _guts)
|
|
}
|
|
|
|
internal subscript(internal position: Index) -> _InternalRun {
|
|
let i = _resolveRun(position)
|
|
return _guts.runs[i]
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs {
|
|
// FIXME: Make public, with a better name. (Probably no need to state "run" -- `index(containing:)`?)
|
|
internal func indexOfRun(at position: AttributedString.Index) -> Index {
|
|
precondition(
|
|
position._value >= _strBounds.lowerBound && position._value <= _strBounds.upperBound,
|
|
"AttributedString index is out of bounds")
|
|
let r = _guts.findRun(at: position._value)
|
|
return Index(_runIndex: r.runIndex, stringIndex: r.start)
|
|
}
|
|
|
|
internal func _firstOfMatchingRuns(
|
|
with i: _InternalRuns.Index,
|
|
comparing attributeNames: [String]
|
|
) -> _InternalRuns.Index {
|
|
precondition(!attributeNames.isEmpty)
|
|
let attributes = _guts.runs[i].attributes
|
|
var j = i
|
|
while j.offset > startIndex._runOffset {
|
|
let prev = _guts.runs.index(before: j)
|
|
let a = _guts.runs[prev].attributes
|
|
if !attributes.isEqual(to: a, comparing: attributeNames) {
|
|
return j
|
|
}
|
|
j = prev
|
|
}
|
|
return j
|
|
}
|
|
|
|
internal func _lastOfMatchingRuns(
|
|
with i: _InternalRuns.Index,
|
|
comparing attributeNames: [String]
|
|
) -> _InternalRuns.Index {
|
|
precondition(!attributeNames.isEmpty)
|
|
precondition(i.offset < endIndex._runOffset)
|
|
let attributes = _guts.runs[i].attributes
|
|
var j = i
|
|
while true {
|
|
let next = _guts.runs.index(after: j)
|
|
if next.offset == endIndex._runOffset { break }
|
|
let a = _guts.runs[next].attributes
|
|
if !attributes.isEqual(to: a, comparing: attributeNames) {
|
|
return j
|
|
}
|
|
j = next
|
|
}
|
|
return j
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension AttributedString.Runs {
|
|
internal func _slicedRunBoundary(
|
|
after i: AttributedString.Index,
|
|
attributeNames: [String],
|
|
constraints: [AttributeRunBoundaries]
|
|
) -> AttributedString.Index {
|
|
precondition(
|
|
self._strBounds.contains(i._value),
|
|
"AttributedString index is out of bounds")
|
|
precondition(!attributeNames.isEmpty)
|
|
let r = _guts.findRun(at: i._value)
|
|
let endRun = _lastOfMatchingRuns(with: r.runIndex, comparing: attributeNames)
|
|
let utf8End = endRun.utf8Offset + _guts.runs[endRun].length
|
|
let stringEnd = Swift.min(
|
|
_guts.string.utf8.index(r.start, offsetBy: utf8End - r.start.utf8Offset),
|
|
_strBounds.upperBound)
|
|
return .init(_guts.string._firstConstraintBreak(in: i._value ..< stringEnd, with: constraints))
|
|
}
|
|
|
|
internal func _slicedRunBoundary(
|
|
before i: AttributedString.Index,
|
|
attributeNames: [String],
|
|
constraints: [AttributeRunBoundaries]
|
|
) -> AttributedString.Index {
|
|
precondition(
|
|
i._value > self._strBounds.lowerBound && i._value <= self._strBounds.upperBound,
|
|
"AttributedString index is out of bounds")
|
|
precondition(!attributeNames.isEmpty)
|
|
let r = _guts.runs.index(atUTF8Offset: i._value.utf8Offset - 1)
|
|
let startRun = _firstOfMatchingRuns(with: r.index, comparing: attributeNames)
|
|
let stringStart = Swift.max(
|
|
_guts.string.utf8.index(i._value, offsetBy: startRun.utf8Offset - i._value.utf8Offset),
|
|
_strBounds.lowerBound)
|
|
return .init(_guts.string._lastConstraintBreak(in: stringStart ..< i._value, with: constraints))
|
|
}
|
|
|
|
internal func _slicedRunBoundary(
|
|
roundingDown i: AttributedString.Index,
|
|
attributeNames: [String],
|
|
constraints: [AttributeRunBoundaries]
|
|
) -> (index: AttributedString.Index, runIndex: AttributedString._InternalRuns.Index) {
|
|
precondition(
|
|
i._value >= self._strBounds.lowerBound && i._value <= self._strBounds.upperBound,
|
|
"AttributedString index is out of bounds")
|
|
precondition(!attributeNames.isEmpty)
|
|
let r = _guts.findRun(at: i._value)
|
|
if r.runIndex.offset == endIndex._runOffset {
|
|
return (i, r.runIndex)
|
|
}
|
|
let startRun = _firstOfMatchingRuns(with: r.runIndex, comparing: attributeNames)
|
|
let stringStart = Swift.max(
|
|
_guts.string.utf8.index(r.start, offsetBy: startRun.utf8Offset - r.start.utf8Offset),
|
|
_strBounds.lowerBound)
|
|
|
|
let j = _guts.string.unicodeScalars.index(after: i._value)
|
|
let last = _guts.string._lastConstraintBreak(in: stringStart ..< j, with: constraints)
|
|
return (.init(last), r.runIndex)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
|
extension BigString {
|
|
internal func _firstConstraintBreak(
|
|
in range: Range<Index>,
|
|
with constraints: [AttributedString.AttributeRunBoundaries]
|
|
) -> Index {
|
|
guard !constraints.isEmpty, !range.isEmpty else { return range.upperBound }
|
|
|
|
var r = range
|
|
if
|
|
constraints.contains(.paragraph),
|
|
let firstBreak = self._findFirstParagraphBoundary(in: r)
|
|
{
|
|
r = r.lowerBound ..< firstBreak
|
|
}
|
|
|
|
if constraints._containsScalarConstraint {
|
|
// Note: we need to slice runs on matching scalars even if they don't carry
|
|
// the attributes we're looking for.
|
|
let scalars: [UnicodeScalar] = constraints.compactMap { $0._constrainedScalar }
|
|
if let firstBreak = self.unicodeScalars[r]._findFirstScalarBoundary(for: scalars) {
|
|
r = r.lowerBound ..< firstBreak
|
|
}
|
|
}
|
|
|
|
return r.upperBound
|
|
}
|
|
|
|
internal func _lastConstraintBreak(
|
|
in range: Range<Index>,
|
|
with constraints: [AttributedString.AttributeRunBoundaries]
|
|
) -> Index {
|
|
guard !constraints.isEmpty, !range.isEmpty else { return range.lowerBound }
|
|
|
|
var r = range
|
|
if
|
|
constraints.contains(.paragraph),
|
|
let lastBreak = self._findLastParagraphBoundary(in: r)
|
|
{
|
|
r = lastBreak ..< r.upperBound
|
|
}
|
|
|
|
if constraints._containsScalarConstraint {
|
|
// Note: we need to slice runs on matching scalars even if they don't carry
|
|
// the attributes we're looking for.
|
|
let scalars: [UnicodeScalar] = constraints.compactMap { $0._constrainedScalar }
|
|
if let lastBreak = self.unicodeScalars[r]._findLastScalarBoundary(for: scalars) {
|
|
r = lastBreak ..< r.upperBound
|
|
}
|
|
}
|
|
|
|
return r.lowerBound
|
|
}
|
|
|
|
internal func _findFirstParagraphBoundary(in range: Range<Index>) -> Index? {
|
|
self.utf8[range]._getBlock(for: [.findEnd], in: range.lowerBound ..< range.lowerBound).end
|
|
}
|
|
|
|
internal func _findLastParagraphBoundary(in range: Range<Index>) -> Index? {
|
|
guard range.upperBound > startIndex else { return nil }
|
|
let lower = self.utf8.index(before: range.upperBound)
|
|
return self.utf8[range]._getBlock(for: [.findStart], in: lower ..< range.upperBound).start
|
|
}
|
|
}
|
|
|
|
extension BigSubstring.UnicodeScalarView {
|
|
internal func _findFirstScalarBoundary(for scalars: [UnicodeScalar]) -> Index? {
|
|
var i = self.startIndex
|
|
guard i < self.endIndex else { return nil }
|
|
if scalars.contains(self[i]) {
|
|
return self.index(after: i)
|
|
}
|
|
while true {
|
|
self.formIndex(after: &i)
|
|
guard i < self.endIndex else { break }
|
|
if scalars.contains(self[i]) {
|
|
return i
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
internal func _findLastScalarBoundary(for scalars: [UnicodeScalar]) -> Index? {
|
|
guard !isEmpty else { return nil }
|
|
var i = self.index(before: self.endIndex)
|
|
if scalars.contains(self[i]) {
|
|
return i
|
|
}
|
|
while i > self.startIndex {
|
|
let j = self.index(before: i)
|
|
if scalars.contains(self[j]) {
|
|
return i
|
|
}
|
|
i = j
|
|
}
|
|
return nil
|
|
}
|
|
}
|