Skip to content

Commit de786c4

Browse files
feat(frontend): display compound license expressions
When the backend provides a `package_license_expression` (SPDX-style string for compound licenses), display it as the license label instead of the flat list of individual licenses. For packages with only simple licenses the behavior is unchanged. Bump frontend index version to 47 to match the new schema.
1 parent dbba21b commit de786c4

3 files changed

Lines changed: 163 additions & 29 deletions

File tree

frontend/src/Page/Packages.elm

Lines changed: 154 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type alias ResultItemSource =
8181
, description : Maybe String
8282
, longDescription : Maybe String
8383
, licenses : List ResultPackageLicense
84+
, licenseExpression : Maybe LicenseExpression
8485
, maintainers : List ResultPackageMaintainer
8586
, teams : List ResultPackageTeam
8687
, platforms : List String
@@ -101,6 +102,19 @@ type alias ResultPackageLicense =
101102
}
102103

103104

105+
{-| Structured license expression tree mirroring the compound-license
106+
operators introduced in NixOS/nixpkgs#468378. AND licenses must all be
107+
satisfied; OR licenses offer a choice; WITH applies an exception; PLUS is
108+
"or any later version".
109+
-}
110+
type LicenseExpression
111+
= LicenseLeaf { fullName : String, url : Maybe String }
112+
| LicenseAnd (List LicenseExpression)
113+
| LicenseOr (List LicenseExpression)
114+
| LicenseWith LicenseExpression LicenseExpression
115+
| LicensePlus LicenseExpression
116+
117+
104118
type alias ResultPackageMaintainer =
105119
{ name : Maybe String
106120
, email : Maybe String
@@ -405,35 +419,38 @@ viewResultItem nixosChannels channel showInstallDetails show item =
405419
|> Maybe.withDefault []
406420
)
407421
++ renderSource item nixosChannels channel trapClick createShortDetailsItem createGithubUrl
408-
++ (let
409-
licenses =
410-
item.source.licenses
411-
|> List.filterMap
412-
(\license ->
413-
case license.url of
414-
Nothing ->
415-
Maybe.map text license.fullName
416-
417-
Just url ->
418-
Just
419-
(createShortDetailsItem
420-
(Maybe.withDefault "Unknown" license.fullName)
421-
url
422-
)
423-
)
424-
in
425-
optionals (not (List.isEmpty licenses))
426-
[ li []
427-
(text
428-
(if List.length licenses == 1 then
429-
"License: "
430-
431-
else
432-
"Licenses: "
422+
++ (case item.source.licenseExpression of
423+
Just expression ->
424+
[ li []
425+
(text "License: "
426+
:: renderLicenseExpression createShortDetailsItem expression
433427
)
434-
:: List.intersperse (text "") licenses
435-
)
436-
]
428+
]
429+
430+
Nothing ->
431+
let
432+
licenses =
433+
item.source.licenses
434+
|> List.filterMap
435+
(\license ->
436+
case license.url of
437+
Nothing ->
438+
Maybe.map text license.fullName
439+
440+
Just url ->
441+
Just
442+
(createShortDetailsItem
443+
(Maybe.withDefault "Unknown" license.fullName)
444+
url
445+
)
446+
)
447+
in
448+
optionals (not (List.isEmpty licenses))
449+
[ li []
450+
(text "License: "
451+
:: List.intersperse (text "") licenses
452+
)
453+
]
437454
)
438455
)
439456
)
@@ -936,6 +953,74 @@ viewResultItem nixosChannels channel showInstallDetails show item =
936953
)
937954

938955

956+
renderLicenseExpression :
957+
(String -> String -> Html Msg)
958+
-> LicenseExpression
959+
-> List (Html Msg)
960+
renderLicenseExpression mkLink expr =
961+
case expr of
962+
LicenseLeaf { fullName, url } ->
963+
case url of
964+
Just u ->
965+
[ mkLink fullName u ]
966+
967+
Nothing ->
968+
[ text fullName ]
969+
970+
LicenseAnd children ->
971+
renderJoined mkLink "AND" children
972+
973+
LicenseOr children ->
974+
renderJoined mkLink "OR" children
975+
976+
LicenseWith license exception ->
977+
renderChild mkLink license
978+
++ [ text " ", span [ class "license-operator" ] [ text "WITH" ], text " " ]
979+
++ renderChild mkLink exception
980+
981+
LicensePlus license ->
982+
renderChild mkLink license ++ [ text "+" ]
983+
984+
985+
renderJoined :
986+
(String -> String -> Html Msg)
987+
-> String
988+
-> List LicenseExpression
989+
-> List (Html Msg)
990+
renderJoined mkLink op children =
991+
let
992+
separator =
993+
[ text " ", span [ class "license-operator" ] [ text op ], text " " ]
994+
in
995+
children
996+
|> List.map (renderChild mkLink)
997+
|> List.intersperse separator
998+
|> List.concat
999+
1000+
1001+
{-| Render a sub-expression, parenthesising compound children so the
1002+
precedence is unambiguous.
1003+
-}
1004+
renderChild :
1005+
(String -> String -> Html Msg)
1006+
-> LicenseExpression
1007+
-> List (Html Msg)
1008+
renderChild mkLink expr =
1009+
let
1010+
inner =
1011+
renderLicenseExpression mkLink expr
1012+
in
1013+
case expr of
1014+
LicenseLeaf _ ->
1015+
inner
1016+
1017+
LicensePlus _ ->
1018+
inner
1019+
1020+
_ ->
1021+
text "(" :: inner ++ [ text ")" ]
1022+
1023+
9391024
renderSource :
9401025
Search.ResultItem ResultItemSource
9411026
-> List NixOSChannel
@@ -1120,6 +1205,9 @@ decodeResultItemSource =
11201205
|> Json.Decode.Pipeline.required "package_description" (Json.Decode.nullable Json.Decode.string)
11211206
|> Json.Decode.Pipeline.required "package_longDescription" (Json.Decode.nullable Json.Decode.string)
11221207
|> Json.Decode.Pipeline.required "package_license" (Json.Decode.list decodeResultPackageLicense)
1208+
|> Json.Decode.Pipeline.optional "package_license_expression"
1209+
(Json.Decode.nullable decodeLicenseExpression)
1210+
Nothing
11231211
|> Json.Decode.Pipeline.required "package_maintainers" (Json.Decode.list decodeResultPackageMaintainer)
11241212
|> Json.Decode.Pipeline.required "package_teams" (Json.Decode.list decodeResultPackageTeam)
11251213
|> Json.Decode.Pipeline.required "package_platforms" (Json.Decode.map filterPlatforms (Json.Decode.list Json.Decode.string))
@@ -1225,6 +1313,44 @@ decodeResultPackageLicense =
12251313
(Json.Decode.field "url" (Json.Decode.nullable Json.Decode.string))
12261314

12271315

1316+
decodeLicenseExpression : Json.Decode.Decoder LicenseExpression
1317+
decodeLicenseExpression =
1318+
let
1319+
recurse =
1320+
Json.Decode.lazy (\_ -> decodeLicenseExpression)
1321+
in
1322+
Json.Decode.field "kind" Json.Decode.string
1323+
|> Json.Decode.andThen
1324+
(\kind ->
1325+
case kind of
1326+
"leaf" ->
1327+
Json.Decode.map2
1328+
(\n u -> LicenseLeaf { fullName = n, url = u })
1329+
(Json.Decode.field "fullName" Json.Decode.string)
1330+
(Json.Decode.field "url" (Json.Decode.nullable Json.Decode.string))
1331+
1332+
"and" ->
1333+
Json.Decode.map LicenseAnd
1334+
(Json.Decode.field "licenses" (Json.Decode.list recurse))
1335+
1336+
"or" ->
1337+
Json.Decode.map LicenseOr
1338+
(Json.Decode.field "licenses" (Json.Decode.list recurse))
1339+
1340+
"with" ->
1341+
Json.Decode.map2 LicenseWith
1342+
(Json.Decode.field "license" recurse)
1343+
(Json.Decode.field "exception" recurse)
1344+
1345+
"plus" ->
1346+
Json.Decode.map LicensePlus
1347+
(Json.Decode.field "license" recurse)
1348+
1349+
other ->
1350+
Json.Decode.fail ("Unknown license expression kind: " ++ other)
1351+
)
1352+
1353+
12281354
decodeResultPackageMaintainer : Json.Decode.Decoder ResultPackageMaintainer
12291355
decodeResultPackageMaintainer =
12301356
Json.Decode.map3 ResultPackageMaintainer

frontend/src/index.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,3 +984,11 @@ a:focus-visible {
984984
.search-result-button > li:has(.option-badge-column):first-child:not(:last-child):after {
985985
content: none;
986986
}
987+
988+
// SPDX operators in compound license expressions -- bold weight to
989+
// distinguish AND / OR / WITH from license names.
990+
.license-operator {
991+
font-weight: bold;
992+
font-size: 0.85em;
993+
opacity: 0.75;
994+
}

version.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
Frontend index version used by the UI when querying Elasticsearch
99
Keep this at the old version while 'import' populates a new index, then update to switch traffic
1010
*/
11-
frontend = "46";
11+
frontend = "47";
1212
}

0 commit comments

Comments
 (0)