Clean up repository structure and release docs
This commit is contained in:
@@ -26,10 +26,10 @@ APP_DIR="${APP_DIR:-$DIST_DIR/$APP_NAME.app}"
|
||||
CONTENTS_DIR="$APP_DIR/Contents"
|
||||
MACOS_DIR="$CONTENTS_DIR/MacOS"
|
||||
RESOURCES_DIR="$CONTENTS_DIR/Resources"
|
||||
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/ihatepdf-profile-transparent.png}"
|
||||
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/assets/app-icon.png}"
|
||||
if [[ ! -f "$ICON_SOURCE" ]]; then
|
||||
echo "Missing app icon source: $ICON_SOURCE" >&2
|
||||
echo "Set ICON_SOURCE to the path of a transparent PNG icon (for example: $ROOT_DIR/ihatepdf-profile-transparent.png)." >&2
|
||||
echo "Set ICON_SOURCE to the path of a transparent PNG icon (for example: $ROOT_DIR/assets/app-icon.png)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ BUNDLE_ID="${BUNDLE_ID:-net.akkolli.ihatepdfs}"
|
||||
APP_SIGNING_IDENTITY="${APP_SIGNING_IDENTITY:-}"
|
||||
INSTALLER_SIGNING_IDENTITY="${INSTALLER_SIGNING_IDENTITY:-}"
|
||||
PROVISIONING_PROFILE="${PROVISIONING_PROFILE:-}"
|
||||
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-$ROOT_DIR/Signing/IHatePDFs-AppStore.entitlements}"
|
||||
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-$ROOT_DIR/signing/IHatePDFs-AppStore.entitlements}"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
PKG_PATH="${PKG_PATH:-$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-appstore.pkg}"
|
||||
VALIDATE_WITH_ALTOOL="${VALIDATE_WITH_ALTOOL:-0}"
|
||||
|
||||
@@ -8,6 +8,7 @@ APP_NAME="I Hate PDFs"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
STAGING_DIR="$DIST_DIR/tiny"
|
||||
ARCHS_TO_BUILD="${ARCHS_TO_BUILD:-arm64 x86_64}"
|
||||
PER_ARCH_INSTALLER_MAX_BYTES="${PER_ARCH_INSTALLER_MAX_BYTES:-400000}"
|
||||
|
||||
if ! command -v xz >/dev/null 2>&1; then
|
||||
echo "xz is required to build size-gated tiny archives with architecture filters." >&2
|
||||
@@ -39,6 +40,27 @@ compression_args_for_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
file_size() {
|
||||
stat -f '%z' "$1"
|
||||
}
|
||||
|
||||
verify_under_budget() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || {
|
||||
echo "missing $path" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
local bytes
|
||||
bytes="$(file_size "$path")"
|
||||
if (( bytes >= PER_ARCH_INSTALLER_MAX_BYTES )); then
|
||||
echo "$path is $bytes bytes; per-architecture installer budget is < $PER_ARCH_INSTALLER_MAX_BYTES bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: $path is $bytes bytes (< $PER_ARCH_INSTALLER_MAX_BYTES)."
|
||||
}
|
||||
|
||||
for ARCH in $ARCHS_TO_BUILD; do
|
||||
APP_DIR="$STAGING_DIR/$ARCH/$APP_NAME.app"
|
||||
ARCHIVE_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-$ARCH.tar.xz"
|
||||
@@ -59,6 +81,5 @@ for ARCH in $ARCHS_TO_BUILD; do
|
||||
| env XZ_OPT= xz "${XZ_ARGS[@]}" -c > "$ARCHIVE_PATH"
|
||||
|
||||
echo "Created $ARCHIVE_PATH"
|
||||
verify_under_budget "$ARCHIVE_PATH"
|
||||
done
|
||||
|
||||
"$ROOT_DIR/scripts/verify-release-size.sh"
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import PDFKit
|
||||
|
||||
let defaultInputPath = "dist/annotation-verification.pdf"
|
||||
let arguments = Array(CommandLine.arguments.dropFirst())
|
||||
let inputURL = URL(fileURLWithPath: arguments.first ?? defaultInputPath)
|
||||
let verificationDate = Date(timeIntervalSince1970: 1_797_000_000)
|
||||
|
||||
if arguments.isEmpty {
|
||||
try generateVerificationPDF(at: inputURL)
|
||||
}
|
||||
|
||||
struct AnnotationSummary {
|
||||
var highlights = 0
|
||||
@@ -49,9 +60,6 @@ enum VerificationError: Error, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
let inputPath = CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf"
|
||||
let inputURL = URL(fileURLWithPath: inputPath)
|
||||
|
||||
guard let document = CGPDFDocument(inputURL as CFURL) else {
|
||||
throw VerificationError.unreadablePDF(inputURL.path)
|
||||
}
|
||||
@@ -289,3 +297,164 @@ func requireTextKeys(
|
||||
try requireString(in: dictionary, key: "T", page: page, index: index)
|
||||
try requireString(in: dictionary, key: "M", page: page, index: index)
|
||||
}
|
||||
|
||||
func generateVerificationPDF(at outputURL: URL) throws {
|
||||
try FileManager.default.createDirectory(
|
||||
at: outputURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let page = PDFPage()
|
||||
let document = PDFDocument()
|
||||
document.insert(page, at: 0)
|
||||
|
||||
let highlight = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
highlight.markupType = .highlight
|
||||
highlight.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.24)
|
||||
highlight.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
highlight,
|
||||
name: "verify-highlight",
|
||||
contents: "This is a standards-compliant PDF highlight comment.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(highlight)
|
||||
|
||||
let selectedTextComment = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
selectedTextComment.markupType = .highlight
|
||||
selectedTextComment.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.10)
|
||||
selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
|
||||
standardize(
|
||||
selectedTextComment,
|
||||
name: "verify-selected-text-comment",
|
||||
contents: "This selected-text comment is saved as standard parent annotation contents.",
|
||||
author: "Professor"
|
||||
)
|
||||
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
|
||||
page.addAnnotation(selectedTextComment)
|
||||
|
||||
let underline = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
|
||||
forType: .underline,
|
||||
withProperties: nil
|
||||
)
|
||||
underline.markupType = .underline
|
||||
underline.color = NSColor(calibratedRed: 0.48, green: 0.53, blue: 0.62, alpha: 0.56)
|
||||
underline.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
underline,
|
||||
name: "verify-underline",
|
||||
contents: "This underline comment should remain openable.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(underline)
|
||||
|
||||
let textNote = PDFAnnotation(
|
||||
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
textNote.iconType = .note
|
||||
textNote.color = NSColor(calibratedRed: 0.64, green: 0.59, blue: 0.49, alpha: 0.90)
|
||||
standardize(
|
||||
textNote,
|
||||
name: "verify-text-note",
|
||||
contents: "This standard PDF text annotation remains visible in common PDF readers.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(textNote)
|
||||
|
||||
let reply = PDFAnnotation(
|
||||
bounds: CGRect(x: 402, y: 586, width: 24, height: 24),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
reply.iconType = .comment
|
||||
reply.color = NSColor(calibratedRed: 0.52, green: 0.58, blue: 0.60, alpha: 0.88)
|
||||
standardize(
|
||||
reply,
|
||||
name: "verify-reply",
|
||||
contents: "This reply is saved as PDF reply data without drawing an extra page icon.",
|
||||
author: "Reader"
|
||||
)
|
||||
_ = reply.setValue("verify-text-note", forAnnotationKey: PDFAnnotationKey(rawValue: "IRT"))
|
||||
_ = reply.setValue("R", forAnnotationKey: PDFAnnotationKey(rawValue: "RT"))
|
||||
reply.shouldDisplay = false
|
||||
reply.shouldPrint = false
|
||||
page.addAnnotation(reply)
|
||||
|
||||
let freeText = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 500, width: 260, height: 50),
|
||||
forType: .freeText,
|
||||
withProperties: nil
|
||||
)
|
||||
freeText.font = NSFont.systemFont(ofSize: 13)
|
||||
freeText.fontColor = NSColor(calibratedWhite: 0.22, alpha: 1)
|
||||
freeText.color = NSColor(calibratedRed: 0.91, green: 0.86, blue: 0.75, alpha: 0.32)
|
||||
freeText.alignment = .left
|
||||
let border = PDFBorder()
|
||||
border.lineWidth = 0.75
|
||||
freeText.border = border
|
||||
standardize(
|
||||
freeText,
|
||||
name: "verify-free-text",
|
||||
contents: "Free text remains visible on the PDF page.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(freeText)
|
||||
|
||||
guard document.write(to: outputURL) else {
|
||||
fatalError("Unable to write \(outputURL.path)")
|
||||
}
|
||||
|
||||
let reopened = PDFDocument(url: outputURL)!
|
||||
let annotations = reopened.page(at: 0)!.annotations
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("highlight") == true })
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("selected-text comment") == true })
|
||||
precondition(annotations.contains { matches($0, .underline) && $0.contents?.contains("underline") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("text annotation") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("reply") == true && !$0.shouldDisplay && !$0.shouldPrint })
|
||||
precondition(annotations.contains { matches($0, .freeText) && $0.contents?.contains("Free text") == true })
|
||||
}
|
||||
|
||||
func standardize(
|
||||
_ annotation: PDFAnnotation,
|
||||
name: String,
|
||||
contents: String,
|
||||
author: String
|
||||
) {
|
||||
annotation.contents = contents
|
||||
annotation.userName = author
|
||||
annotation.modificationDate = verificationDate
|
||||
annotation.shouldDisplay = true
|
||||
annotation.shouldPrint = true
|
||||
_ = annotation.setValue(name, forAnnotationKey: .name)
|
||||
_ = annotation.setValue(author, forAnnotationKey: .textLabel)
|
||||
_ = annotation.setValue(verificationDate, forAnnotationKey: .date)
|
||||
_ = annotation.setValue("D:20261215132000Z00'00'", forAnnotationKey: PDFAnnotationKey(rawValue: "CreationDate"))
|
||||
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
|
||||
}
|
||||
|
||||
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
|
||||
[
|
||||
NSValue(point: CGPoint(x: 0, y: height)),
|
||||
NSValue(point: CGPoint(x: width, y: height)),
|
||||
NSValue(point: CGPoint(x: 0, y: 0)),
|
||||
NSValue(point: CGPoint(x: width, y: 0))
|
||||
]
|
||||
}
|
||||
|
||||
func matches(_ annotation: PDFAnnotation, _ subtype: PDFAnnotationSubtype) -> Bool {
|
||||
guard let type = annotation.type else { return false }
|
||||
let raw = subtype.rawValue
|
||||
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
|
||||
return type == raw || type == normalized
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/release-version.sh"
|
||||
|
||||
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"
|
||||
PER_ARCH_INSTALLER_MAX_BYTES="${PER_ARCH_INSTALLER_MAX_BYTES:-400000}"
|
||||
PER_ARCH_INSTALLER_EXTENSION="${PER_ARCH_INSTALLER_EXTENSION:-tar.xz}"
|
||||
|
||||
fail() {
|
||||
echo "release size verification failed: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
file_size() {
|
||||
stat -f '%z' "$1"
|
||||
}
|
||||
|
||||
verify_under_budget() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || fail "missing $path"
|
||||
|
||||
local bytes
|
||||
bytes="$(file_size "$path")"
|
||||
if (( bytes >= PER_ARCH_INSTALLER_MAX_BYTES )); then
|
||||
fail "$path is $bytes bytes; per-architecture installer budget is < $PER_ARCH_INSTALLER_MAX_BYTES bytes"
|
||||
fi
|
||||
|
||||
echo "OK: $path is $bytes bytes (< $PER_ARCH_INSTALLER_MAX_BYTES)."
|
||||
}
|
||||
|
||||
if (( $# > 0 )); then
|
||||
for artifact in "$@"; do
|
||||
verify_under_budget "$artifact"
|
||||
done
|
||||
else
|
||||
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-arm64.$PER_ARCH_INSTALLER_EXTENSION"
|
||||
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-x86_64.$PER_ARCH_INSTALLER_EXTENSION"
|
||||
fi
|
||||
@@ -1,167 +0,0 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import PDFKit
|
||||
|
||||
let outputURL = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf")
|
||||
try FileManager.default.createDirectory(
|
||||
at: outputURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let page = PDFPage()
|
||||
let document = PDFDocument()
|
||||
document.insert(page, at: 0)
|
||||
|
||||
let verificationDate = Date(timeIntervalSince1970: 1_797_000_000)
|
||||
|
||||
let highlight = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
highlight.markupType = .highlight
|
||||
highlight.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.24)
|
||||
highlight.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
highlight,
|
||||
name: "verify-highlight",
|
||||
contents: "This is a standards-compliant PDF highlight comment.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(highlight)
|
||||
|
||||
let selectedTextComment = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
selectedTextComment.markupType = .highlight
|
||||
selectedTextComment.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.10)
|
||||
selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
|
||||
standardize(
|
||||
selectedTextComment,
|
||||
name: "verify-selected-text-comment",
|
||||
contents: "This selected-text comment is saved as standard parent annotation contents.",
|
||||
author: "Professor"
|
||||
)
|
||||
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
|
||||
page.addAnnotation(selectedTextComment)
|
||||
|
||||
let underline = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
|
||||
forType: .underline,
|
||||
withProperties: nil
|
||||
)
|
||||
underline.markupType = .underline
|
||||
underline.color = NSColor(calibratedRed: 0.48, green: 0.53, blue: 0.62, alpha: 0.56)
|
||||
underline.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
underline,
|
||||
name: "verify-underline",
|
||||
contents: "This underline comment should remain openable.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(underline)
|
||||
|
||||
let textNote = PDFAnnotation(
|
||||
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
textNote.iconType = .note
|
||||
textNote.color = NSColor(calibratedRed: 0.64, green: 0.59, blue: 0.49, alpha: 0.90)
|
||||
standardize(
|
||||
textNote,
|
||||
name: "verify-text-note",
|
||||
contents: "This standard PDF text annotation remains visible in common PDF readers.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(textNote)
|
||||
|
||||
let reply = PDFAnnotation(
|
||||
bounds: CGRect(x: 402, y: 586, width: 24, height: 24),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
reply.iconType = .comment
|
||||
reply.color = NSColor(calibratedRed: 0.52, green: 0.58, blue: 0.60, alpha: 0.88)
|
||||
standardize(
|
||||
reply,
|
||||
name: "verify-reply",
|
||||
contents: "This reply is saved as PDF reply data without drawing an extra page icon.",
|
||||
author: "Reader"
|
||||
)
|
||||
_ = reply.setValue("verify-text-note", forAnnotationKey: PDFAnnotationKey(rawValue: "IRT"))
|
||||
_ = reply.setValue("R", forAnnotationKey: PDFAnnotationKey(rawValue: "RT"))
|
||||
reply.shouldDisplay = false
|
||||
reply.shouldPrint = false
|
||||
page.addAnnotation(reply)
|
||||
|
||||
let freeText = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 500, width: 260, height: 50),
|
||||
forType: .freeText,
|
||||
withProperties: nil
|
||||
)
|
||||
freeText.font = NSFont.systemFont(ofSize: 13)
|
||||
freeText.fontColor = NSColor(calibratedWhite: 0.22, alpha: 1)
|
||||
freeText.color = NSColor(calibratedRed: 0.91, green: 0.86, blue: 0.75, alpha: 0.32)
|
||||
freeText.alignment = .left
|
||||
let border = PDFBorder()
|
||||
border.lineWidth = 0.75
|
||||
freeText.border = border
|
||||
standardize(
|
||||
freeText,
|
||||
name: "verify-free-text",
|
||||
contents: "Free text remains visible on the PDF page.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(freeText)
|
||||
|
||||
guard document.write(to: outputURL) else {
|
||||
fatalError("Unable to write \(outputURL.path)")
|
||||
}
|
||||
|
||||
let reopened = PDFDocument(url: outputURL)!
|
||||
let annotations = reopened.page(at: 0)!.annotations
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("highlight") == true })
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("selected-text comment") == true })
|
||||
precondition(annotations.contains { matches($0, .underline) && $0.contents?.contains("underline") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("text annotation") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("reply") == true && !$0.shouldDisplay && !$0.shouldPrint })
|
||||
precondition(annotations.contains { matches($0, .freeText) && $0.contents?.contains("Free text") == true })
|
||||
|
||||
print("Verified standard PDF annotations in \(outputURL.path)")
|
||||
|
||||
func standardize(
|
||||
_ annotation: PDFAnnotation,
|
||||
name: String,
|
||||
contents: String,
|
||||
author: String
|
||||
) {
|
||||
annotation.contents = contents
|
||||
annotation.userName = author
|
||||
annotation.modificationDate = verificationDate
|
||||
annotation.shouldDisplay = true
|
||||
annotation.shouldPrint = true
|
||||
_ = annotation.setValue(name, forAnnotationKey: .name)
|
||||
_ = annotation.setValue(author, forAnnotationKey: .textLabel)
|
||||
_ = annotation.setValue(verificationDate, forAnnotationKey: .date)
|
||||
_ = annotation.setValue("D:20261215132000Z00'00'", forAnnotationKey: PDFAnnotationKey(rawValue: "CreationDate"))
|
||||
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
|
||||
}
|
||||
|
||||
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
|
||||
[
|
||||
NSValue(point: CGPoint(x: 0, y: height)),
|
||||
NSValue(point: CGPoint(x: width, y: height)),
|
||||
NSValue(point: CGPoint(x: 0, y: 0)),
|
||||
NSValue(point: CGPoint(x: width, y: 0))
|
||||
]
|
||||
}
|
||||
|
||||
func matches(_ annotation: PDFAnnotation, _ subtype: PDFAnnotationSubtype) -> Bool {
|
||||
guard let type = annotation.type else { return false }
|
||||
let raw = subtype.rawValue
|
||||
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
|
||||
return type == raw || type == normalized
|
||||
}
|
||||
Reference in New Issue
Block a user