From a6da15fd76c7ecaf06e93d6c715929c7b020b5c4 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 7 Nov 2025 17:16:01 +0100 Subject: [PATCH 1/4] Experimental: expose license detection edit operations. --- app/lib/frontend/handlers/experimental.dart | 3 + app/lib/frontend/templates/package.dart | 88 ++++++++++++++++++++- pkg/web_app/lib/src/foldable.dart | 13 +++ pkg/web_css/lib/src/_pkg.scss | 22 ++++++ 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/app/lib/frontend/handlers/experimental.dart b/app/lib/frontend/handlers/experimental.dart index d745713e2b..958f991e90 100644 --- a/app/lib/frontend/handlers/experimental.dart +++ b/app/lib/frontend/handlers/experimental.dart @@ -14,6 +14,7 @@ const _publicFlags = { final _allFlags = { 'dark-as-default', + 'license', ..._publicFlags.map((x) => x.name), }; @@ -88,6 +89,8 @@ class ExperimentalFlags { bool get isDarkModeDefault => isEnabled('dark-as-default'); + late final isLicenseEnabled = isEnabled('license'); + String encodedAsCookie() => _enabled.join(':'); @override diff --git a/app/lib/frontend/templates/package.dart b/app/lib/frontend/templates/package.dart index 912ee47879..6139f841ee 100644 --- a/app/lib/frontend/templates/package.dart +++ b/app/lib/frontend/templates/package.dart @@ -5,6 +5,8 @@ import 'package:_pub_shared/data/page_data.dart'; import 'package:_pub_shared/search/tags.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:pana/pana.dart'; +import 'package:pub_dev/frontend/request_context.dart'; import 'package:pub_dev/frontend/templates/views/pkg/liked_package_list.dart'; import '../../package/models.dart'; @@ -378,13 +380,91 @@ Tab _installTab(PackagePageData data) { } Tab _licenseTab(PackagePageData data) { - final license = data.hasLicense - ? renderFile(data.asset!, urlResolverFn: data.urlResolverFn) - : d.text('No license file found.'); + final licenses = data.scoreCard.panaReport?.licenses; + final hasEditOpData = + licenses != null && + licenses.isNotEmpty && + licenses.any((l) => l.operations?.isNotEmpty ?? false); + late d.Node content; + if (!data.hasLicense) { + content = d.text('No license file found.'); + } else if (hasEditOpData && + requestContext.experimentalFlags.isLicenseEnabled) { + final text = data.asset!.textContent!; + final opAndLicensePairs = + licenses + .expand((l) => (l.operations ?? []).map((op) => (op, l))) + .toList() + ..sort((a, b) => a.$1.start.compareTo(b.$1.start)); + final nodes = []; + var offset = 0; + for (final (op, _) in opAndLicensePairs) { + if (offset < op.start) { + nodes.add( + d.span( + classes: ['license-op-insert'], + text: text.substring(offset, op.start), + ), + ); + offset = op.start; + } + switch (op.type) { + case TextOpType.delete: + nodes.add( + d.span( + classes: ['license-op-delete', 'license-op-delete-hidden'], + children: [ + d.span( + classes: ['license-op-delete-icon'], + text: '✄', + attributes: {'tabindex': '-1'}, + ), + d.span( + classes: ['license-op-delete-content'], + text: op.content, + ), + ], + ), + ); + break; + case TextOpType.insert: + final end = op.start + op.length; + nodes.add( + d.span( + classes: ['license-op-insert'], + text: text.substring(op.start, end), + ), + ); + offset = end; + break; + case TextOpType.match: + final end = op.start + op.length; + nodes.add( + d.span( + classes: ['license-op-match'], + text: text.substring(op.start, end), + ), + ); + offset = end; + break; + } + } + if (offset < text.length) { + nodes.add( + d.span(classes: ['license-op-insert'], text: text.substring(offset)), + ); + } + content = d.div( + classes: ['highlight'], + child: d.pre(children: nodes), + ); + } else { + content = renderFile(data.asset!, urlResolverFn: data.urlResolverFn); + } return Tab.withContent( id: 'license', title: 'License', - contentNode: d.fragment([d.h2(text: 'License'), license]), + contentNode: d.fragment([d.h2(text: 'License'), content]), isMarkdown: true, ); } diff --git a/pkg/web_app/lib/src/foldable.dart b/pkg/web_app/lib/src/foldable.dart index 1b9c528b84..302a4bbdd1 100644 --- a/pkg/web_app/lib/src/foldable.dart +++ b/pkg/web_app/lib/src/foldable.dart @@ -12,6 +12,7 @@ import 'web_util.dart'; void setupFoldable() { _setEventForFoldable(); _setEventForCheckboxToggle(); + _setEventForLicenseDeleteIcons(); } /// Elements with the `foldable` class provide a folding content: @@ -106,3 +107,15 @@ void _setEventForCheckboxToggle() { }); } } + +/// Setup a toggle event for the delete operation icons in licenses. +void _setEventForLicenseDeleteIcons() { + final icons = document.body! + .querySelectorAll('.license-op-delete-icon') + .toElementList(); + for (final icon in icons) { + icon.onClick.listen((event) { + icon.parentElement!.classList.toggle('license-op-delete-hidden'); + }); + } +} diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index ca2cba9a2e..181625c1f9 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -582,3 +582,25 @@ } } } + +.license-op-delete { + background: rgba(255, 0, 0, 0.2); + + &.license-op-delete-hidden { + .license-op-delete-content { + display: none; + } + } + + .license-op-delete-icon { + cursor: pointer; + } +} + +.license-op-insert { + background: rgba(255, 255, 0, 0.2); +} + +.license-op-match { + background: rgba(0, 255, 0, 0.2); +} From 2c7645f8004cce74e3ff6b3313b272dbbd9a2a45 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 7 Nov 2025 18:01:00 +0100 Subject: [PATCH 2/4] extracted colors --- pkg/web_css/lib/src/_pkg.scss | 6 +++--- pkg/web_css/lib/src/_variables.scss | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 181625c1f9..b38d01b7b5 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -584,7 +584,7 @@ } .license-op-delete { - background: rgba(255, 0, 0, 0.2); + background: var(--pub-license-editop-delete); &.license-op-delete-hidden { .license-op-delete-content { @@ -598,9 +598,9 @@ } .license-op-insert { - background: rgba(255, 255, 0, 0.2); + background: var(--pub-license-editop-insert); } .license-op-match { - background: rgba(0, 255, 0, 0.2); + background: var(--pub-license-editop-match); } diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index 275a1d19a3..5683eef93e 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -101,6 +101,11 @@ // Opacity values used to display monochrome icons. --pub-monochrome-opacity-initial: 0.6; --pub-monochrome-opacity-hover: 1.0; + + // Incomplete colors for license text edit ops. + --pub-license-editop-delete: rgba(255, 0, 0, 0.2); + --pub-license-editop-insert: rgba(255, 255, 0, 0.2); + --pub-license-editop-match: rgba(0, 255, 0, 0.2); } /// Variables that are specific to the light theme. From 4b7a5d162ca7d007527349994ee6988c2eb7f5df Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Mon, 10 Nov 2025 13:17:24 +0100 Subject: [PATCH 3/4] Rename experiment name. --- app/lib/frontend/handlers/experimental.dart | 4 ++-- app/lib/frontend/templates/package.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/frontend/handlers/experimental.dart b/app/lib/frontend/handlers/experimental.dart index 958f991e90..01c7617f97 100644 --- a/app/lib/frontend/handlers/experimental.dart +++ b/app/lib/frontend/handlers/experimental.dart @@ -14,7 +14,7 @@ const _publicFlags = { final _allFlags = { 'dark-as-default', - 'license', + 'expose-licence-diff', ..._publicFlags.map((x) => x.name), }; @@ -89,7 +89,7 @@ class ExperimentalFlags { bool get isDarkModeDefault => isEnabled('dark-as-default'); - late final isLicenseEnabled = isEnabled('license'); + late final isExposeLicenseDiffEnabled = isEnabled('expose-licence-diff'); String encodedAsCookie() => _enabled.join(':'); diff --git a/app/lib/frontend/templates/package.dart b/app/lib/frontend/templates/package.dart index 6139f841ee..faf064a30c 100644 --- a/app/lib/frontend/templates/package.dart +++ b/app/lib/frontend/templates/package.dart @@ -389,7 +389,7 @@ Tab _licenseTab(PackagePageData data) { if (!data.hasLicense) { content = d.text('No license file found.'); } else if (hasEditOpData && - requestContext.experimentalFlags.isLicenseEnabled) { + requestContext.experimentalFlags.isExposeLicenseDiffEnabled) { final text = data.asset!.textContent!; final opAndLicensePairs = licenses From b87cc1f924a8a61c28c042fd5dae90d67ba2f4f3 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Mon, 10 Nov 2025 13:18:44 +0100 Subject: [PATCH 4/4] title attribute shows which kind of content it is --- app/lib/frontend/templates/package.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/lib/frontend/templates/package.dart b/app/lib/frontend/templates/package.dart index faf064a30c..b40c8ff015 100644 --- a/app/lib/frontend/templates/package.dart +++ b/app/lib/frontend/templates/package.dart @@ -404,6 +404,7 @@ Tab _licenseTab(PackagePageData data) { d.span( classes: ['license-op-insert'], text: text.substring(offset, op.start), + attributes: {'title': 'inserted content'}, ), ); offset = op.start; @@ -424,6 +425,7 @@ Tab _licenseTab(PackagePageData data) { text: op.content, ), ], + attributes: {'title': 'deleted content'}, ), ); break; @@ -433,6 +435,7 @@ Tab _licenseTab(PackagePageData data) { d.span( classes: ['license-op-insert'], text: text.substring(op.start, end), + attributes: {'title': 'inserted content'}, ), ); offset = end; @@ -443,6 +446,7 @@ Tab _licenseTab(PackagePageData data) { d.span( classes: ['license-op-match'], text: text.substring(op.start, end), + attributes: {'title': 'matched content'}, ), ); offset = end; @@ -451,7 +455,11 @@ Tab _licenseTab(PackagePageData data) { } if (offset < text.length) { nodes.add( - d.span(classes: ['license-op-insert'], text: text.substring(offset)), + d.span( + classes: ['license-op-insert'], + text: text.substring(offset), + attributes: {'title': 'inserted content'}, + ), ); } content = d.div(