diff --git a/FrontEnd/styles/_images.scss b/FrontEnd/styles/_images.scss
index fd6dac4b4..1754d32c1 100644
--- a/FrontEnd/styles/_images.scss
+++ b/FrontEnd/styles/_images.scss
@@ -42,8 +42,8 @@
--image-incompatible: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQ4Ljk1IDEwLjk1LTkuOS05LjktMTQuMDUgMTQuMDUtMTQuMDUtMTQuMDUtOS45IDkuOSAxNC4wNSAxNC4wNS0xNC4wNSAxNC4wNSA5LjkgOS45IDE0LjA1LTE0LjA1IDE0LjA1IDE0LjA1IDkuOS05LjktMTQuMDUtMTQuMDV6IiBmaWxsPSIjOWE5YTlhIi8+PC9zdmc+');
--image-info: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTMuNTEgMzcuNThoLTcuMTJ2LTE5LjM2aDcuMTJ6bS0uODYtMjEuODVjLS43Ni40MS0xLjY1LjYxLTIuNjcuNjFzLTEuOTMtLjItMi42Ny0uNjFjLS43My0uNC0xLjEtMS4xOC0xLjEtMi4zM3MuMzctMS45OCAxLjEtMi4zOCAxLjYyLS42IDIuNjctLjYgMS45MS4yIDIuNjcuNiAxLjE0IDEuMTkgMS4xNCAyLjM4LS4zOCAxLjkzLTEuMTQgMi4zM3oiIGZpbGw9IiMzNTZmY2UiLz48L3N2Zz4=');
--image-libraries: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQ3LjMyIDEzLjI0LTIwLjE5IDguMjl2MjcuMmwyMC4xOS04LjI5em0tNDQuNjQgMHYyNy4ybDIwLjE5IDguMjl2LTI3LjJ6bTIyLjMyLTExLjk3LTE5Ljg0IDguMTMgMTkuODQgOC4xNCAxOS44NC04LjE0eiIgZmlsbD0iIzJmMmYyZiIvPjwvc3ZnPg==');
+ --image-license: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgLjg0djQ4LjMzaDM4di00OC4zMmgtMzh6bTcuOTQgNy4xMyAzLjI1LTMuOSAzLjI1IDMuOSA0LjcxIDEuODktMi43IDQuMjktLjM0IDUuMDYtNC45Mi0xLjI0LTQuOTIgMS4yNC0uMzQtNS4wNi0yLjctNC4yOXptMjQuMTQgMzEuNThoLTI2LjE4di00LjcyaDI2LjE3djQuNzJ6bS4wMi0xMS4wMWgtMjYuMTh2LTQuNzJoMjYuMTd2NC43MnptMC0xMS4wMWgtOC4zMXYtNC43Mmg4LjMxeiIgZmlsbD0iIzJmMmYyZiIvPjwvc3ZnPg==');
--image-macros: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzJmMmYyZiI+PHBhdGggZD0ibTI2LjI4IDEuMDctNC40MyAxMy42M2gyNC43OGMtMy42OC03LjcyLTExLjM0LTEzLjE1LTIwLjM1LTEzLjYzeiIvPjxwYXRoIGQ9Im0yLjY2IDE2LjM3IDExLjYgOC40MyA3LjY1LTIzLjU3Yy04LjQ4IDEuMTEtMTYuMDEgNi43Mi0xOS4yNSAxNS4xNHoiLz48cGF0aCBkPSJtOS45MSA0My41NyAxMS42LTguNDMtMjAuMDUtMTQuNTZjLTEuNTcgOC40MSAxLjQ1IDE3LjMxIDguNDUgMjIuOTl6Ii8+PHBhdGggZD0ibTM4LjAxIDQ1LjA4LTQuNDMtMTMuNjMtMjAuMDUgMTQuNTZjNy41MSA0LjA5IDE2LjkxIDMuOTcgMjQuNDgtLjkzeiIvPjxwYXRoIGQ9Im00OC4xMyAxOC44MmgtMTQuMzRzNy42NiAyMy41NyA3LjY2IDIzLjU3YzYuMjEtNS44OCA5LjAxLTE0Ljg1IDYuNjgtMjMuNTd6Ii8+PC9nPjwvc3ZnPg==');
- --image-osi: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI4LjAzIDMzLjY4YzQuMzYtMS42NyA2LjUzLTYuNTUgNC44Ni0xMC44OS0xLjY3LTQuMzUtNi41Ni02LjUyLTEwLjkxLTQuODUtNC4zNiAxLjY3LTYuNTMgNi41NS00Ljg2IDEwLjg5Ljg2IDIuMjMgMi42MiAzLjk5IDQuODYgNC44NWwtNS42OSAxNC44Yy0xMi41NS00LjgtMTguODEtMTguODUtMTQtMzEuMzcgNC44MS0xMi41MyAxOC44OC0xOC43OCAzMS40My0xMy45NyAxMi41NCA0LjgxIDE4LjgxIDE4Ljg1IDEzLjk5IDMxLjM4LTIuNDcgNi40My03LjU2IDExLjUtMTMuOTkgMTMuOTdsLTUuNjktMTQuOHoiIGZpbGw9IiMyZjJmMmYiLz48L3N2Zz4=');
--image-plugins: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTM3LjkgNC43NmgtMjUuOGMtNi42NyA0LjI3LTExLjEgMTEuNzMtMTEuMSAyMC4yNHM0LjQzIDE1Ljk3IDExLjEgMjAuMjRoMjUuNzljNi42Ny00LjI2IDExLjEtMTEuNzMgMTEuMS0yMC4yNHMtNC40My0xNS45Ny0xMS4xLTIwLjI0em0tMjAuMTQgMjMuNTloLTQuODd2LTE1LjQ5aDQuODd6bTExLjQ3IDEwLjc4aC04LjQydi0yLjA1YzAtMi4zMSAxLjg5LTQuMjEgNC4yMS00LjIxczQuMjEgMS44OSA0LjIxIDQuMjF6bTcuODgtMTAuNzhoLTQuODd2LTE1LjQ5aDQuODd6IiBmaWxsPSIjMmYyZjJmIi8+PC9zdmc+');
--image-podcast: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMjUiIGN5PSIyNSIgZmlsbD0iIzM1NmZjZSIgcj0iMjQuNSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Im0yNSA4LjFjLTUuOTIgMC0xMC43NiA0Ljg0LTEwLjc2IDEwLjc2djYuNzFjMCA1LjkyIDQuODQgMTAuNzYgMTAuNzYgMTAuNzYgNS45MiAwIDEwLjc2LTQuODQgMTAuNzYtMTAuNzZ2LTYuNzFjMC01LjkyLTQuODQtMTAuNzYtMTAuNzYtMTAuNzZ6bS0zLjM1IDE2LjM3YzAgLjYyLS41IDEuMTItMS4xMiAxLjEycy0xLjEyLS41LTEuMTItMS4xMnYtNC41MWMwLS42Mi41LTEuMTIgMS4xMi0xLjEyczEuMTIuNSAxLjEyIDEuMTJ6bTQuNDcgMS40OGMwIC42Mi0uNSAxLjEyLTEuMTIgMS4xMnMtMS4xMi0uNS0xLjEyLTEuMTJ2LTcuNDZjMC0uNjIuNS0xLjEyIDEuMTItMS4xMnMxLjEyLjUgMS4xMiAxLjEyem00LjQ4LTEuNDhjMCAuNjItLjUgMS4xMi0xLjEyIDEuMTJzLTEuMTItLjUtMS4xMi0xLjEydi00LjUxYzAtLjYyLjUtMS4xMiAxLjEyLTEuMTJzMS4xMi41IDEuMTIgMS4xMnoiLz48cGF0aCBkPSJtMjUgNDEuOWMtNy40OCAwLTE0LjA5LTUuMTgtMTUuNzEtMTIuMzEtLjE5LS44Ni4zNC0xLjcxIDEuMi0xLjkxLjg2LS4xOSAxLjcxLjM0IDEuOTEgMS4yIDEuMjkgNS43IDYuNTkgOS44MyAxMi42IDkuODNzMTEuMzEtNC4xMyAxMi42LTkuODNjLjE5LS44NiAxLjA1LTEuMzkgMS45MS0xLjJzMS40IDEuMDUgMS4yIDEuOTFjLTEuNjIgNy4xMy04LjIzIDEyLjMxLTE1LjcxIDEyLjMxeiIvPjwvZz48L3N2Zz4=');
--image-question: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTIuMzIgMzguMDZjLS44Ny41OS0xLjg5Ljg5LTMuMDcuODlzLTIuMjktLjMtMy4xNi0uODljLS44Ny0uNi0xLjMtMS41Ni0xLjMtMi45IDAtMS40LjQzLTIuMzkgMS4zLTIuOTZzMS45Mi0uODUgMy4xNi0uODUgMi4yMS4yOCAzLjA3Ljg1Yy44Ny41NyAxLjMgMS41NSAxLjMgMi45NnMtLjQzIDIuMy0xLjMgMi45em03LjcxLTE4Ljk0Yy0uMzQuODYtLjg3IDEuNjctMS42MSAyLjQyLS43My43NS0xLjcgMS41LTIuOSAyLjIyLS44NS41LTEuNTEuOTMtMiAxLjI5cy0uODMuNy0xLjA0IDEuMDNjLS4yLjMzLS4zMS43MS0uMzEgMS4xM3YxLjAxaC02LjQ5di0xLjcxYzAtLjgyLjExLTEuNTYuMzMtMi4yMnMuNjEtMS4yOSAxLjE3LTEuODggMS4zNS0xLjE5IDIuMzUtMS43OWMuOTYtLjU3IDEuNjYtMS4wNiAyLjExLTEuNDhzLjc1LS44Ljg5LTEuMTUuMjEtLjcuMjEtMS4wNmMwLS41Ni0uMjctMS0uOC0xLjMyLS41NC0uMzItMS4yMy0uNDktMi4wOS0uNDktMS4wNyAwLTIuMjMuMTgtMy40OC41NHMtMi41Ny44OS0zLjk4IDEuNTlsLTIuOTQtNS40YzEuNTktLjg2IDMuMjktMS41NCA1LjA5LTIuMDVzMy43Ny0uNzYgNS45LS43NmMzLjIgMCA1LjY4LjY2IDcuNDQgMS45OXMyLjY0IDMuMDggMi42NCA1LjI1YzAgMS4wMy0uMTcgMS45OC0uNTEgMi44NHoiIGZpbGw9IiMyZjJmMmYiLz48L3N2Zz4=');
@@ -82,8 +82,8 @@
--image-incompatible: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQ4Ljk1IDEwLjk1LTkuOS05LjktMTQuMDUgMTQuMDUtMTQuMDUtMTQuMDUtOS45IDkuOSAxNC4wNSAxNC4wNS0xNC4wNSAxNC4wNSA5LjkgOS45IDE0LjA1LTE0LjA1IDE0LjA1IDE0LjA1IDkuOS05LjktMTQuMDUtMTQuMDV6IiBmaWxsPSIjMmYyZjJmIi8+PC9zdmc+');
--image-info: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTMuNTEgMzcuNThoLTcuMTJ2LTE5LjM2aDcuMTJ6bS0uODYtMjEuODVjLS43Ni40MS0xLjY1LjYxLTIuNjcuNjFzLTEuOTMtLjItMi42Ny0uNjFjLS43My0uNC0xLjEtMS4xOC0xLjEtMi4zM3MuMzctMS45OCAxLjEtMi4zOCAxLjYyLS42IDIuNjctLjYgMS45MS4yIDIuNjcuNiAxLjE0IDEuMTkgMS4xNCAyLjM4LS4zOCAxLjkzLTEuMTQgMi4zM3oiIGZpbGw9IiMxNjk0ZjEiLz48L3N2Zz4=');
--image-libraries: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQ3LjMyIDEzLjI0LTIwLjE5IDguMjl2MjcuMmwyMC4xOS04LjI5em0tNDQuNjQgMHYyNy4ybDIwLjE5IDguMjl2LTI3LjJ6bTIyLjMyLTExLjk3LTE5Ljg0IDguMTMgMTkuODQgOC4xNCAxOS44NC04LjE0eiIgZmlsbD0iI2YxZjFmMSIvPjwvc3ZnPg==');
+ --image-license: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgLjg0djQ4LjMzaDM4di00OC4zMmgtMzh6bTcuOTQgNy4xMyAzLjI1LTMuOSAzLjI1IDMuOSA0LjcxIDEuODktMi43IDQuMjktLjM0IDUuMDYtNC45Mi0xLjI0LTQuOTIgMS4yNC0uMzQtNS4wNi0yLjctNC4yOXptMjQuMTQgMzEuNThoLTI2LjE4di00LjcyaDI2LjE3djQuNzJ6bS4wMi0xMS4wMWgtMjYuMTh2LTQuNzJoMjYuMTd2NC43MnptMC0xMS4wMWgtOC4zMXYtNC43Mmg4LjMxeiIgZmlsbD0iI2YxZjFmMSIvPjwvc3ZnPg==');
--image-macros: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2YxZjFmMSI+PHBhdGggZD0ibTI2LjI4IDEuMDctNC40MyAxMy42M2gyNC43OGMtMy42OC03LjcyLTExLjM0LTEzLjE1LTIwLjM1LTEzLjYzeiIvPjxwYXRoIGQ9Im0yLjY2IDE2LjM3IDExLjYgOC40MyA3LjY1LTIzLjU3Yy04LjQ4IDEuMTEtMTYuMDEgNi43Mi0xOS4yNSAxNS4xNHoiLz48cGF0aCBkPSJtOS45MSA0My41NyAxMS42LTguNDMtMjAuMDUtMTQuNTZjLTEuNTcgOC40MSAxLjQ1IDE3LjMxIDguNDUgMjIuOTl6Ii8+PHBhdGggZD0ibTM4LjAxIDQ1LjA4LTQuNDMtMTMuNjMtMjAuMDUgMTQuNTZjNy41MSA0LjA5IDE2LjkxIDMuOTcgMjQuNDgtLjkzeiIvPjxwYXRoIGQ9Im00OC4xMyAxOC44MmgtMTQuMzRzNy42NiAyMy41NyA3LjY2IDIzLjU3YzYuMjEtNS44OCA5LjAxLTE0Ljg1IDYuNjgtMjMuNTd6Ii8+PC9nPjwvc3ZnPg==');
- --image-osi: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI4LjAzIDMzLjY4YzQuMzYtMS42NyA2LjUzLTYuNTUgNC44Ni0xMC44OS0xLjY3LTQuMzUtNi41Ni02LjUyLTEwLjkxLTQuODUtNC4zNiAxLjY3LTYuNTMgNi41NS00Ljg2IDEwLjg5Ljg2IDIuMjMgMi42MiAzLjk5IDQuODYgNC44NWwtNS42OSAxNC44Yy0xMi41NS00LjgtMTguODEtMTguODUtMTQtMzEuMzcgNC44MS0xMi41MyAxOC44OC0xOC43OCAzMS40My0xMy45NyAxMi41NCA0LjgxIDE4LjgxIDE4Ljg1IDEzLjk5IDMxLjM4LTIuNDcgNi40My03LjU2IDExLjUtMTMuOTkgMTMuOTdsLTUuNjktMTQuOHoiIGZpbGw9IiNmMWYxZjEiLz48L3N2Zz4=');
--image-plugins: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTM3LjkgNC43NmgtMjUuOGMtNi42NyA0LjI3LTExLjEgMTEuNzMtMTEuMSAyMC4yNHM0LjQzIDE1Ljk3IDExLjEgMjAuMjRoMjUuNzljNi42Ny00LjI2IDExLjEtMTEuNzMgMTEuMS0yMC4yNHMtNC40My0xNS45Ny0xMS4xLTIwLjI0em0tMjAuMTQgMjMuNTloLTQuODd2LTE1LjQ5aDQuODd6bTExLjQ3IDEwLjc4aC04LjQydi0yLjA1YzAtMi4zMSAxLjg5LTQuMjEgNC4yMS00LjIxczQuMjEgMS44OSA0LjIxIDQuMjF6bTcuODgtMTAuNzhoLTQuODd2LTE1LjQ5aDQuODd6IiBmaWxsPSIjZjFmMWYxIi8+PC9zdmc+');
--image-podcast: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMjUiIGN5PSIyNSIgZmlsbD0iIzE2OTRmMSIgcj0iMjQuNSIvPjxnIGZpbGw9IiMwNzA3MTAiPjxwYXRoIGQ9Im0yNSA4LjFjLTUuOTIgMC0xMC43NiA0Ljg0LTEwLjc2IDEwLjc2djYuNzFjMCA1LjkyIDQuODQgMTAuNzYgMTAuNzYgMTAuNzYgNS45MiAwIDEwLjc2LTQuODQgMTAuNzYtMTAuNzZ2LTYuNzFjMC01LjkyLTQuODQtMTAuNzYtMTAuNzYtMTAuNzZ6bS0zLjM1IDE2LjM3YzAgLjYyLS41IDEuMTItMS4xMiAxLjEycy0xLjEyLS41LTEuMTItMS4xMnYtNC41MWMwLS42Mi41LTEuMTIgMS4xMi0xLjEyczEuMTIuNSAxLjEyIDEuMTJ6bTQuNDcgMS40OGMwIC42Mi0uNSAxLjEyLTEuMTIgMS4xMnMtMS4xMi0uNS0xLjEyLTEuMTJ2LTcuNDZjMC0uNjIuNS0xLjEyIDEuMTItMS4xMnMxLjEyLjUgMS4xMiAxLjEyem00LjQ4LTEuNDhjMCAuNjItLjUgMS4xMi0xLjEyIDEuMTJzLTEuMTItLjUtMS4xMi0xLjEydi00LjUxYzAtLjYyLjUtMS4xMiAxLjEyLTEuMTJzMS4xMi41IDEuMTIgMS4xMnoiLz48cGF0aCBkPSJtMjUgNDEuOWMtNy40OCAwLTE0LjA5LTUuMTgtMTUuNzEtMTIuMzEtLjE5LS44Ni4zNC0xLjcxIDEuMi0xLjkxLjg2LS4xOSAxLjcxLjM0IDEuOTEgMS4yIDEuMjkgNS43IDYuNTkgOS44MyAxMi42IDkuODNzMTEuMzEtNC4xMyAxMi42LTkuODNjLjE5LS44NiAxLjA1LTEuMzkgMS45MS0xLjJzMS40IDEuMDUgMS4yIDEuOTFjLTEuNjIgNy4xMy04LjIzIDEyLjMxLTE1LjcxIDEyLjMxeiIvPjwvZz48L3N2Zz4=');
--image-question: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTIuMzIgMzguMDZjLS44Ny41OS0xLjg5Ljg5LTMuMDcuODlzLTIuMjktLjMtMy4xNi0uODljLS44Ny0uNi0xLjMtMS41Ni0xLjMtMi45IDAtMS40LjQzLTIuMzkgMS4zLTIuOTZzMS45Mi0uODUgMy4xNi0uODUgMi4yMS4yOCAzLjA3Ljg1Yy44Ny41NyAxLjMgMS41NSAxLjMgMi45NnMtLjQzIDIuMy0xLjMgMi45em03LjcxLTE4Ljk0Yy0uMzQuODYtLjg3IDEuNjctMS42MSAyLjQyLS43My43NS0xLjcgMS41LTIuOSAyLjIyLS44NS41LTEuNTEuOTMtMiAxLjI5cy0uODMuNy0xLjA0IDEuMDNjLS4yLjMzLS4zMS43MS0uMzEgMS4xM3YxLjAxaC02LjQ5di0xLjcxYzAtLjgyLjExLTEuNTYuMzMtMi4yMnMuNjEtMS4yOSAxLjE3LTEuODggMS4zNS0xLjE5IDIuMzUtMS43OWMuOTYtLjU3IDEuNjYtMS4wNiAyLjExLTEuNDhzLjc1LS44Ljg5LTEuMTUuMjEtLjcuMjEtMS4wNmMwLS41Ni0uMjctMS0uOC0xLjMyLS41NC0uMzItMS4yMy0uNDktMi4wOS0uNDktMS4wNyAwLTIuMjMuMTgtMy40OC41NHMtMi41Ny44OS0zLjk4IDEuNTlsLTIuOTQtNS40YzEuNTktLjg2IDMuMjktMS41NCA1LjA5LTIuMDVzMy43Ny0uNzYgNS45LS43NmMzLjIgMCA1LjY4LjY2IDcuNDQgMS45OXMyLjY0IDMuMDggMi42NCA1LjI1YzAgMS4wMy0uMTcgMS45OC0uNTEgMi44NHoiIGZpbGw9IiNkYWRhZGEiLz48L3N2Zz4=');
diff --git a/FrontEnd/styles/_package.scss b/FrontEnd/styles/_package.scss
index 1f1ce1f10..783c78acd 100644
--- a/FrontEnd/styles/_package.scss
+++ b/FrontEnd/styles/_package.scss
@@ -105,18 +105,11 @@
}
li.license {
- background-image: var(--image-osi);
+ background-image: var(--image-license);
- &.warning {
- background-image: var(--image-warning);
- }
-
- &.error {
- background-image: var(--image-error);
- }
-
- .no-license {
+ &.no-license {
color: var(--red-text);
+ background-image: var(--image-error);
}
}
diff --git a/Resources/Markdown/faq.md b/Resources/Markdown/faq.md
index e676e145a..b1435af35 100644
--- a/Resources/Markdown/faq.md
+++ b/Resources/Markdown/faq.md
@@ -14,7 +14,6 @@ description: Frequently Asked Questions about the Swift Package Index
- [What about the GitHub Package Registry?](#package-registry)
- [How is the Swift language and platform support calculated?](#language-and-platforms)
- [Can the Swift Package Index host my package's documentation?](#documentation)
-- [Why are certain licenses highlighted?](#licenses)
- [Can I contribute to the Swift Package Index?](#contributing)
- [Is there an API?](#api)
- [Why are package versions missing?](#missing-versions)
@@ -90,36 +89,6 @@ The Swift Package Index build system can generate and host DocC documentation an
---
-
Why are certain licenses highlighted?
-
-If a package's license shows with an orange or red exclamation mark icon, it is for one of three reasons:
-
-1. The package has no license.
-2. We have been unable to automatically detect the software license used by the package.
-3. The license chosen by the package authors may be incompatible with the App Store.
-
-#### No License
-
-If the package license is showing as "No License" with a red exclamation icon, we could not find _any_ license information in the package repository.
-
-Using a package that does not have a license presents a significant legal risk. Unlicensed code is not open-source, and the original author reserves all rights by default. For more information, read [this great blog post](https://expressionengine.com/blog/the-truth-about-the-risks-of-unlicensed-software) on using unlicensed code.
-
-If you are the package author, you can fix this by adding a `LICENSE` file with an open-source license in your package's repository. The Swift Package Index will update license metadata a few hours after you add the license.
-
-#### Unknown License
-
-If the package license is showing as "Unknown License" with an orange exclamation icon, we have been unable to automatically detect a license in the package repository.
-
-There could be one of several reasons automatic detection failed. The package may use a commercial or closed-source license, or it could be that GitHub's license detection algorithm has failed. Before using a package with an unknown license, you should check the package repository for a `LICENSE` file and ensure you understand the terms that the package author has defined.
-
-If you are the author of a package showing with an unknown license and believe it should show a valid open-source license, please [see GitHub's documentation](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository#detecting-a-license). The Swift Package Index will update licenses a few hours after GitHub recognises a valid license.
-
-#### Incompatible license
-
-If the package license shows with an orange exclamation icon but does _not_ say "No License" or "Unknown License", then the package is using a license which may be incompatible with how the App Store works, such as a GPL-style license. If you are considering shipping software that includes a package licensed with one of these licenses to the App Store, you should be aware that using code licensed under one of these licences may present a legal risk.
-
----
-
Can I contribute to the Swift Package Index?
Absolutely. The Swift Package Index is [open-source](https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server), and we’d love it if you wanted to help us make it better. Please see the [guide to contributing in our README](https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/blob/main/README.md#contributing) for more information.
@@ -186,7 +155,7 @@ For example, a query of [`charts last_activity:>=2021-02-01`](https://swiftpacka
#### Querying License values
-You can search for packages that have a license which is [compatible with the App Store](#licenses) by simply querying [`license:compatible`](https://swiftpackageindex.com/search?query=license%3Acompatible).
+You can search for packages that have a known license by querying [`license:known`](/search?query=license%3Aknown). You can also search for unknown licenses with [`license:unknown`](/search?query=license%3Aunknown), or packages where we could not find a license with [`license:none`](/search?query=license%3Anone).
You can also specify a specific license you wish a package to have from one of the [built-in options](https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/blob/main/Sources/App/Models/License.swift). For example, a query of [`license:lgpl-2.1`](https://swiftpackageindex.com/search?query=license%3Algpl-2.1) matches any package licensed under the LGPL 2.1.
diff --git a/Resources/SVGs/license~dark.svg b/Resources/SVGs/license~dark.svg
new file mode 100644
index 000000000..1fd7a27fb
--- /dev/null
+++ b/Resources/SVGs/license~dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/SVGs/license~light.svg b/Resources/SVGs/license~light.svg
new file mode 100644
index 000000000..471007de0
--- /dev/null
+++ b/Resources/SVGs/license~light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Resources/SVGs/osi~dark.svg b/Resources/SVGs/osi~dark.svg
deleted file mode 100644
index 7619079d6..000000000
--- a/Resources/SVGs/osi~dark.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/Resources/SVGs/osi~light.svg b/Resources/SVGs/osi~light.svg
deleted file mode 100644
index d17c5630b..000000000
--- a/Resources/SVGs/osi~light.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/Sources/App/Core/Score.swift b/Sources/App/Core/Score.swift
index 04c2be308..44c08e3b3 100644
--- a/Sources/App/Core/Score.swift
+++ b/Sources/App/Core/Score.swift
@@ -55,12 +55,12 @@ enum Score {
scoreBreakdown[.archive] = 20
}
- // Is the license open-source and compatible with the App Store?
+ // Is the license a known open-source license?
switch candidate.licenseKind {
- case .compatibleWithAppStore:
+ case .known:
scoreBreakdown[.license] = 10
- case .incompatibleWithAppStore:
- scoreBreakdown[.license] = 3
+ case .unknown:
+ scoreBreakdown[.license] = 5
default: break;
}
diff --git a/Sources/App/Core/SearchFilter/Filters/LicenseSearchFilter.swift b/Sources/App/Core/SearchFilter/Filters/LicenseSearchFilter.swift
index f1f528fcf..0df89ec01 100644
--- a/Sources/App/Core/SearchFilter/Filters/LicenseSearchFilter.swift
+++ b/Sources/App/Core/SearchFilter/Filters/LicenseSearchFilter.swift
@@ -19,9 +19,9 @@ import Foundation
///
/// Examples:
/// ```
-/// license:compatible - The license is compatible with the app store
-/// license:!compatible - The license is unknown, none is provided, or the one provided is not compatible with the app store
-/// license:mit - The package specifically uses the MIT license (any can be used)
+/// license:known - The license is identified as a known open source license
+/// license:!known - The license is either unknown or not found
+/// license:mit - The package specifically uses the MIT license
/// ```
struct LicenseSearchFilter: SearchFilterProtocol {
static let key: SearchFilter.Key = .license
diff --git a/Sources/App/Migrations/084/UpdateRepositoriesLicenseAndScoreDetails.swift b/Sources/App/Migrations/084/UpdateRepositoriesLicenseAndScoreDetails.swift
new file mode 100644
index 000000000..b741a25ca
--- /dev/null
+++ b/Sources/App/Migrations/084/UpdateRepositoriesLicenseAndScoreDetails.swift
@@ -0,0 +1,79 @@
+// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Fluent
+import SQLKit
+
+
+struct UpdateRepositoriesLicenseAndScoreDetails: AsyncMigration {
+ func prepare(on database: Database) async throws {
+ guard let db = database as? SQLDatabase else {
+ fatalError("Database must be an SQLDatabase ('as? SQLDatabase' must succeed)")
+ }
+
+ // Rename "other" -> "unknown" in repositories.license
+ try await db.raw("""
+ UPDATE repositories
+ SET license = 'unknown'
+ WHERE license = 'other'
+ """).run()
+
+ // Rename "compatible" -> "known" and "incompatible" -> "known" in the score_details JSON
+ try await db.raw("""
+ UPDATE packages
+ SET score_details = jsonb_set(score_details::jsonb, '{licenseKind}', '"known"')
+ WHERE score_details::jsonb->>'licenseKind' IN ('compatible', 'incompatible')
+ """).run()
+
+ // Rename "other" -> "unknown" in the score_details JSON
+ try await db.raw("""
+ UPDATE packages
+ SET score_details = jsonb_set(score_details::jsonb, '{licenseKind}', '"unknown"')
+ WHERE score_details::jsonb->>'licenseKind' = 'other'
+ """).run()
+
+ try await db.raw("REFRESH MATERIALIZED VIEW search").run()
+ }
+
+ func revert(on database: Database) async throws {
+ guard let db = database as? SQLDatabase else {
+ fatalError("Database must be an SQLDatabase ('as? SQLDatabase' must succeed)")
+ }
+
+ // Revert "unknown" -> "other" in repositories.license
+ try await db.raw("""
+ UPDATE repositories
+ SET license = 'other'
+ WHERE license = 'unknown'
+ """).run()
+
+ // This change is impossible to revert cleanly as we can't easily distinguish
+ // previously-incompatible from previously-compatible. This reverts everything
+ // to "known", which will then be corrected during future score recalculations.
+ try await db.raw("""
+ UPDATE packages
+ SET score_details = jsonb_set(score_details::jsonb, '{licenseKind}', '"compatible"')
+ WHERE score_details::jsonb->>'licenseKind' = 'known'
+ """).run()
+
+ // Revert "unknown" -> "other"
+ try await db.raw("""
+ UPDATE packages
+ SET score_details = jsonb_set(score_details::jsonb, '{licenseKind}', '"other"')
+ WHERE score_details::jsonb->>'licenseKind' = 'unknown'
+ """).run()
+
+ try await db.raw("REFRESH MATERIALIZED VIEW search").run()
+ }
+}
diff --git a/Sources/App/Models/License.swift b/Sources/App/Models/License.swift
index 46c5df547..e6dd0630b 100644
--- a/Sources/App/Models/License.swift
+++ b/Sources/App/Models/License.swift
@@ -67,8 +67,8 @@ enum License: String, Codable, Equatable, CaseIterable {
case zlib
// These are special cases, not license types
- case other // An unknown or unidentified license
- case none // Actually unlicensed code!
+ case unknown // A license has been found but is either in a non-standard format or does not match a known license.
+ case none // No license was found, assumed to be without a license.
var fullName: String {
switch self {
@@ -123,8 +123,8 @@ enum License: String, Codable, Equatable, CaseIterable {
case .wtfpl: return "Do What The F**k You Want To Public License"
case .zlib: return "zLib License"
- case .other: return "Unknown or Unrecognised License"
- case .none: return "No License"
+ case .unknown: return "Unknown or Unrecognised License"
+ case .none: return "No License Found"
}
}
@@ -181,49 +181,31 @@ enum License: String, Codable, Equatable, CaseIterable {
case .wtfpl: return "DWTFYWTPL"
case .zlib: return "zLib"
- case .other: return "Unknown license"
- case .none: return "No license"
+ case .unknown: return "Unknown license"
+ case .none: return "No license found"
}
}
var licenseKind: Kind {
switch self {
- case .other:
- return .other
+ case .unknown:
+ return .unknown
case .none:
return .none
- case .agpl_3_0,
- .cecill_2_1,
- .cern_ohl_s_2_0,
- .cern_ohl_w_2_0,
- .eupl_1_1,
- .eupl_1_2,
- .gfdl_1_3,
- .gpl,
- .gpl_2_0,
- .gpl_3_0,
- .lgpl,
- .lgpl_2_1,
- .lgpl_3_0,
- .lppl_1_3c,
- .ms_rl,
- .osl_3_0: return .incompatibleWithAppStore
- default: return .compatibleWithAppStore
+ default: return .known
}
}
enum Kind: String, Codable {
case none
- case other
- case incompatibleWithAppStore = "incompatible"
- case compatibleWithAppStore = "compatible"
+ case unknown
+ case known
var userFacingString: String {
switch self {
case .none: return "not defined"
- case .other: return "unknown"
- case .incompatibleWithAppStore: return "incompatible with the App Store"
- case .compatibleWithAppStore: return "compatible with the App Store"
+ case .unknown: return "unknown"
+ case .known: return "known"
}
}
}
@@ -232,7 +214,7 @@ enum License: String, Codable, Equatable, CaseIterable {
extension License {
init(from dto: Github.Metadata.LicenseInfo?) {
if let key = dto?.key {
- self = License(rawValue: key) ?? .other
+ self = License(rawValue: key) ?? .unknown
} else {
self = .none
}
diff --git a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift
index 41db21957..2cc428e14 100644
--- a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift
+++ b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift
@@ -59,7 +59,7 @@ extension API.PackageController.GetRoute.Model {
func licenseListItem() -> Node {
let licenseDescription: Node = {
switch license.licenseKind {
- case .compatibleWithAppStore, .incompatibleWithAppStore:
+ case .known:
return .group(
.unwrap(licenseUrl, {
.a(
@@ -70,58 +70,27 @@ extension API.PackageController.GetRoute.Model {
}),
.text(" licensed")
)
- case .other:
+ case .unknown:
return .unwrap(licenseUrl, {
.a(.href($0), .text(license.shortName))
})
case .none:
- return .span(
- .class(license.licenseKind.cssClass),
- .text(license.shortName)
- )
+ return .text(license.shortName)
}
}()
let licenseClass: String = {
switch license.licenseKind {
- case .compatibleWithAppStore:
+ case .known, .unknown:
return "license"
- case .incompatibleWithAppStore, .other:
- return "license warning"
- case .none:
- return "license error"
- }
- }()
-
- let moreInfoLink: Node = {
- switch license.licenseKind {
- case .compatibleWithAppStore:
- return .empty
- case .incompatibleWithAppStore:
- return .a(
- .class("more-info"),
- .href(SiteURL.faq.relativeURL(anchor: "licenses")),
- "Why might the \(license.shortName) be problematic?"
- )
- case .other:
- return .a(
- .class("more-info"),
- .href(SiteURL.faq.relativeURL(anchor: "licenses")),
- "Why is this package's license unknown?"
- )
case .none:
- return .a(
- .class("more-info"),
- .href(SiteURL.faq.relativeURL(anchor: "licenses")),
- "Why should you not use unlicensed code?"
- )
+ return "license no-license"
}
}()
return .li(
.class(licenseClass),
- licenseDescription,
- moreInfoLink
+ licenseDescription
)
}
@@ -664,17 +633,6 @@ extension API.PackageController.GetRoute.Model.Activity {
// MARK: - General helpers
-private extension License.Kind {
- var cssClass: String {
- switch self {
- case .none: return "no-license"
- case .incompatibleWithAppStore, .other: return "incompatible_license"
- case .compatibleWithAppStore: return "compatible_license"
- }
- }
-}
-
-
private extension CompatibilityMatrix.BuildResult where T: BuildResultPresentable {
var cellNode: Node {
.div(
diff --git a/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift b/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift
index a6fa3bdb8..8476791f5 100644
--- a/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift
+++ b/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex+Model.swift
@@ -103,7 +103,11 @@ private extension Score.Details {
case .archive:
return "Repository is \(isArchived ? "" : "not") archived."
case .license:
- return "\(licenseKind == .compatibleWithAppStore ? "" : "No ")OSI-compatible license which is compatible with the App Store."
+ switch licenseKind {
+ case .known: return "Has a known license."
+ case .unknown: return "Has an unknown license."
+ case .none: return "No license found."
+ }
case .releases:
return "Has \(pluralizedCount: releaseCount, singular: "release")."
case .stars:
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 8afbccf51..49362664e 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -350,6 +350,9 @@ public func configure(_ app: Application, databaseHost: String? = nil, databaseP
do { // Migration 083 - Add `key` and unique constraint to `custom_collections`
app.migrations.add(UpdateCustomCollectionAddKey())
}
+ do { // Migration 084 - Update licenses with `other` -> `unknown` and `compatible`/`incompatible` -> `known`
+ app.migrations.add(UpdateRepositoriesLicenseAndScoreDetails())
+ }
app.asyncCommands.use(Analyze.Command(), as: "analyze")
app.asyncCommands.use(CreateRestfileCommand(), as: "create-restfile")
diff --git a/Tests/AppTests/LicenseTests.swift b/Tests/AppTests/LicenseTests.swift
index 4e26ed97c..c7f188439 100644
--- a/Tests/AppTests/LicenseTests.swift
+++ b/Tests/AppTests/LicenseTests.swift
@@ -38,54 +38,33 @@ extension AllTests.LicenseTests {
#expect(License(from: Github.Metadata.LicenseInfo(key: "ofl-1.1")) == .ofl_1_1)
#expect(License(from: Github.Metadata.LicenseInfo(key: "upl-1.0")) == .upl_1_0)
#expect(License(from: Github.Metadata.LicenseInfo(key: "vim")) == .vim)
- #expect(License(from: Github.Metadata.LicenseInfo(key: "other")) == .other)
+ #expect(License(from: Github.Metadata.LicenseInfo(key: "other")) == .unknown)
#expect(License(from: .none) == .none)
}
@Test func init_from_dto_unknown() throws {
- // ensure unknown licenses are mapped to `.other`
- #expect(License(from: Github.Metadata.LicenseInfo(key: "non-existing license")) == .other)
+ // ensure unknown licenses are mapped to `.unknown`
+ #expect(License(from: Github.Metadata.LicenseInfo(key: "non-existing license")) == .unknown)
}
@Test func fullName() throws {
#expect(License.mit.fullName == "MIT License")
#expect(License.agpl_3_0.fullName == "GNU Affero General Public License v3.0")
- #expect(License.other.fullName == "Unknown or Unrecognised License")
- #expect(License.none.fullName == "No License")
+ #expect(License.unknown.fullName == "Unknown or Unrecognised License")
+ #expect(License.none.fullName == "No License Found")
}
@Test func shortName() throws {
#expect(License.mit.shortName == "MIT")
#expect(License.agpl_3_0.shortName == "AGPL 3.0")
- #expect(License.other.shortName == "Unknown license")
- #expect(License.none.shortName == "No license")
+ #expect(License.unknown.shortName == "Unknown license")
+ #expect(License.none.shortName == "No license found")
}
- @Test func isCompatibleWithAppStore() throws {
- // Compatible
- #expect(License.mit.licenseKind == .compatibleWithAppStore)
- #expect(License.mit_0.licenseKind == .compatibleWithAppStore)
- #expect(License.apache_2_0.licenseKind == .compatibleWithAppStore)
- #expect(License.blueoak_1_0_0.licenseKind == .compatibleWithAppStore)
- #expect(License.bsd_2_clause_patent.licenseKind == .compatibleWithAppStore)
- #expect(License.epl_2_0.licenseKind == .compatibleWithAppStore)
- #expect(License.cern_ohl_p_2_0.licenseKind == .compatibleWithAppStore)
- #expect(License.upl_1_0.licenseKind == .compatibleWithAppStore)
- // Incompatible
- #expect(License.agpl_3_0.licenseKind == .incompatibleWithAppStore)
- #expect(License.cecill_2_1.licenseKind == .incompatibleWithAppStore)
- #expect(License.cern_ohl_s_2_0.licenseKind == .incompatibleWithAppStore)
- #expect(License.cern_ohl_w_2_0.licenseKind == .incompatibleWithAppStore)
- #expect(License.eupl_1_1.licenseKind == .incompatibleWithAppStore)
- #expect(License.eupl_1_2.licenseKind == .incompatibleWithAppStore)
- #expect(License.gfdl_1_3.licenseKind == .incompatibleWithAppStore)
- #expect(License.gpl_2_0.licenseKind == .incompatibleWithAppStore)
- #expect(License.lgpl_3_0.licenseKind == .incompatibleWithAppStore)
- #expect(License.lppl_1_3c.licenseKind == .incompatibleWithAppStore)
- #expect(License.ms_rl.licenseKind == .incompatibleWithAppStore)
- #expect(License.osl_3_0.licenseKind == .incompatibleWithAppStore)
- // Special
- #expect(License.other.licenseKind == .other)
+ @Test func licenseKind() throws {
+ #expect(License.mit.licenseKind == .known)
+ #expect(License.agpl_3_0.licenseKind == .known)
+ #expect(License.unknown.licenseKind == .unknown)
#expect(License.none.licenseKind == .none)
}
diff --git a/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift b/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift
index a684e897e..30191443b 100644
--- a/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift
+++ b/Tests/AppTests/Mocks/MaintainerInfoIndex+mock.swift
@@ -35,7 +35,7 @@ extension Score.Details {
static var mock: Self {
@Dependency(\.date.now) var now
return .init(
- licenseKind: .compatibleWithAppStore,
+ licenseKind: .known,
releaseCount: 10,
likeCount: 300,
isArchived: false,
diff --git a/Tests/AppTests/ScoreTests.swift b/Tests/AppTests/ScoreTests.swift
index 7e1a2f9ca..600d9eb11 100644
--- a/Tests/AppTests/ScoreTests.swift
+++ b/Tests/AppTests/ScoreTests.swift
@@ -32,7 +32,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 20)
- #expect(Score.computeBreakdown(.init(licenseKind: .incompatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .unknown,
releaseCount: 0,
likeCount: 0,
isArchived: false,
@@ -41,8 +41,8 @@ extension AllTests.ScoreTests {
hasDocumentation: false,
hasReadme: false,
numberOfContributors: 0,
- hasTestTargets: false)).score == 23)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ hasTestTargets: false)).score == 25)
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 0,
likeCount: 0,
isArchived: false,
@@ -52,7 +52,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 30)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 10,
likeCount: 0,
isArchived: false,
@@ -62,7 +62,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 40)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 10,
likeCount: 50,
isArchived: false,
@@ -72,7 +72,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 50)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 10,
likeCount: 50,
isArchived: true,
@@ -82,7 +82,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 30)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -92,7 +92,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 87)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -102,7 +102,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 89)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -112,7 +112,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 92)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -122,7 +122,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 92)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -132,7 +132,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 97)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -142,7 +142,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 102)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -152,7 +152,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 107)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -162,7 +162,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 122)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -172,7 +172,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 5,
hasTestTargets: false)).score == 127)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -182,7 +182,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 20,
hasTestTargets: false)).score == 132)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -192,7 +192,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: false)).score == 107)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
@@ -202,7 +202,7 @@ extension AllTests.ScoreTests {
hasReadme: false,
numberOfContributors: 0,
hasTestTargets: true)).score == 112)
- #expect(Score.computeBreakdown(.init(licenseKind: .compatibleWithAppStore,
+ #expect(Score.computeBreakdown(.init(licenseKind: .known,
releaseCount: 20,
likeCount: 20_000,
isArchived: false,
diff --git a/Tests/AppTests/SearchFilterTests.swift b/Tests/AppTests/SearchFilterTests.swift
index 9cda4ba26..8d62629fd 100644
--- a/Tests/AppTests/SearchFilterTests.swift
+++ b/Tests/AppTests/SearchFilterTests.swift
@@ -137,7 +137,7 @@ extension AllTests.SearchFilterTests {
value: "1970-01-01"))
#expect(filter.key == .lastActivity)
#expect(filter.predicate == .init(operator: .equal,
- bindableValue: .value("1970-01-01"),
+ bindableValue: .value("1970-01-01 00:00:00 +0000"),
displayValue: "1 Jan 1970"))
// test view representation
@@ -163,7 +163,7 @@ extension AllTests.SearchFilterTests {
value: "1970-01-01"))
#expect(filter.key == .lastCommit)
#expect(filter.predicate == .init(operator: .equal,
- bindableValue: .value("1970-01-01"),
+ bindableValue: .value("1970-01-01 00:00:00 +0000"),
displayValue: "1 Jan 1970"))
// test view representation
@@ -183,22 +183,24 @@ extension AllTests.SearchFilterTests {
}
}
- @Test func licenseFilter_compatible() async throws {
+ @Test func licenseFilter_known() async throws {
try await withSPIApp { app in
let filter = try LicenseSearchFilter(expression: .init(operator: .is,
- value: "compatible"))
+ value: "known"))
+ let expectedLicenses = ["0bsd", "afl-3.0", "agpl-3.0", "apache-2.0", "artistic-2.0", "blueoak-1.0.0", "bsd-2-clause", "bsd-2-clause-patent", "bsd-3-clause", "bsd-3-clause-clear", "bsd-4-clause", "bsl-1.0", "cc", "cc-by-4.0", "cc-by-sa-4.0", "cc0-1.0", "cecill-2.1", "cern-ohl-p-2.0", "cern-ohl-s-2.0", "cern-ohl-w-2.0", "ecl-2.0", "epl-1.0", "epl-2.0", "eupl-1.1", "eupl-1.2", "gfdl-1.3", "gpl", "gpl-2.0", "gpl-3.0", "isc", "lgpl", "lgpl-2.1", "lgpl-3.0", "lppl-1.3c", "mit", "mit-0", "mpl-2.0", "ms-pl", "ms-rl", "mulanpsl-2.0", "ncsa", "odbl-1.0", "ofl-1.1", "osl-3.0", "postgresql", "upl-1.0", "unlicense", "vim", "wtfpl", "zlib"]
+
#expect(filter.key == .license)
#expect(filter.predicate == .init(operator: .in,
- bindableValue: .array(["0bsd", "afl-3.0", "apache-2.0", "artistic-2.0", "blueoak-1.0.0", "bsd-2-clause", "bsd-2-clause-patent", "bsd-3-clause", "bsd-3-clause-clear", "bsd-4-clause", "bsl-1.0", "cc", "cc-by-4.0", "cc-by-sa-4.0", "cc0-1.0", "cern-ohl-p-2.0", "ecl-2.0", "epl-1.0", "epl-2.0", "isc", "mit", "mit-0", "mpl-2.0", "ms-pl", "mulanpsl-2.0", "ncsa", "odbl-1.0", "ofl-1.1", "postgresql", "upl-1.0", "unlicense", "vim", "wtfpl", "zlib"]),
- displayValue: "compatible with the App Store"))
+ bindableValue: .array(expectedLicenses),
+ displayValue: "known"))
// test view representation
- #expect(filter.viewModel.description == "license is compatible with the App Store")
+ #expect(filter.viewModel.description == "license is known")
// test sql representation
#expect(app.db.renderSQL(filter.leftHandSide) == #""license""#)
#expect(app.db.renderSQL(filter.sqlOperator) == "IN")
- #expect(app.db.binds(filter.rightHandSide) == ["0bsd", "afl-3.0", "apache-2.0", "artistic-2.0", "blueoak-1.0.0", "bsd-2-clause", "bsd-2-clause-patent", "bsd-3-clause", "bsd-3-clause-clear", "bsd-4-clause", "bsl-1.0", "cc", "cc-by-4.0", "cc-by-sa-4.0", "cc0-1.0", "cern-ohl-p-2.0", "ecl-2.0", "epl-1.0", "epl-2.0", "isc", "mit", "mit-0", "mpl-2.0", "ms-pl", "mulanpsl-2.0", "ncsa", "odbl-1.0", "ofl-1.1", "postgresql", "upl-1.0", "unlicense", "vim", "wtfpl", "zlib"])
+ #expect(app.db.binds(filter.rightHandSide) == expectedLicenses)
}
}
@@ -235,29 +237,10 @@ extension AllTests.SearchFilterTests {
#expect(
try LicenseSearchFilter(
expression: .init(operator: .is,
- value: "Compatible")).bindableValue == ["0bsd", "afl-3.0", "apache-2.0", "artistic-2.0", "blueoak-1.0.0", "bsd-2-clause", "bsd-2-clause-patent", "bsd-3-clause", "bsd-3-clause-clear", "bsd-4-clause", "bsl-1.0", "cc", "cc-by-4.0", "cc-by-sa-4.0", "cc0-1.0", "cern-ohl-p-2.0", "ecl-2.0", "epl-1.0", "epl-2.0", "isc", "mit", "mit-0", "mpl-2.0", "ms-pl", "mulanpsl-2.0", "ncsa", "odbl-1.0", "ofl-1.1", "postgresql", "upl-1.0", "unlicense", "vim", "wtfpl", "zlib"]
+ value: "Known")).bindableValue == ["0bsd", "afl-3.0", "agpl-3.0", "apache-2.0", "artistic-2.0", "blueoak-1.0.0", "bsd-2-clause", "bsd-2-clause-patent", "bsd-3-clause", "bsd-3-clause-clear", "bsd-4-clause", "bsl-1.0", "cc", "cc-by-4.0", "cc-by-sa-4.0", "cc0-1.0", "cecill-2.1", "cern-ohl-p-2.0", "cern-ohl-s-2.0", "cern-ohl-w-2.0", "ecl-2.0", "epl-1.0", "epl-2.0", "eupl-1.1", "eupl-1.2", "gfdl-1.3", "gpl", "gpl-2.0", "gpl-3.0", "isc", "lgpl", "lgpl-2.1", "lgpl-3.0", "lppl-1.3c", "mit", "mit-0", "mpl-2.0", "ms-pl", "ms-rl", "mulanpsl-2.0", "ncsa", "odbl-1.0", "ofl-1.1", "osl-3.0", "postgresql", "upl-1.0", "unlicense", "vim", "wtfpl", "zlib"]
)
}
- @Test func licenseFilter_incompatible() async throws {
- try await withSPIApp { app in
- let filter = try LicenseSearchFilter(expression: .init(operator: .is,
- value: "incompatible"))
- #expect(filter.key == .license)
- #expect(filter.predicate == .init(operator: .in,
- bindableValue: .array(["agpl-3.0", "cecill-2.1", "cern-ohl-s-2.0", "cern-ohl-w-2.0", "eupl-1.1", "eupl-1.2", "gfdl-1.3", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0", "lppl-1.3c", "ms-rl", "osl-3.0"]),
- displayValue: "incompatible with the App Store"))
-
- // test view representation
- #expect(filter.viewModel.description == "license is incompatible with the App Store")
-
- // test sql representation
- #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#)
- #expect(app.db.renderSQL(filter.sqlOperator) == "IN")
- #expect(app.db.binds(filter.rightHandSide) == ["agpl-3.0", "cecill-2.1", "cern-ohl-s-2.0", "cern-ohl-w-2.0", "eupl-1.1", "eupl-1.2", "gfdl-1.3", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0", "lppl-1.3c", "ms-rl", "osl-3.0"])
- }
- }
-
@Test func licenseFilter_none() async throws {
try await withSPIApp { app in
let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "none"))
@@ -276,12 +259,12 @@ extension AllTests.SearchFilterTests {
}
}
- @Test func licenseFilter_other() async throws {
+ @Test func licenseFilter_unknown() async throws {
try await withSPIApp { app in
- let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "other"))
+ let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "unknown"))
#expect(filter.key == .license)
#expect(filter.predicate == .init(operator: .in,
- bindableValue: .array(["other"]),
+ bindableValue: .array(["unknown"]),
displayValue: "unknown"))
// test view representation
@@ -290,7 +273,7 @@ extension AllTests.SearchFilterTests {
// test sql representation
#expect(app.db.renderSQL(filter.leftHandSide) == #""license""#)
#expect(app.db.renderSQL(filter.sqlOperator) == "IN")
- #expect(app.db.binds(filter.rightHandSide) == ["other"])
+ #expect(app.db.binds(filter.rightHandSide) == ["unknown"])
}
}
@@ -309,7 +292,7 @@ extension AllTests.SearchFilterTests {
value: "ios"))
#expect(filter.key == .platform)
#expect(filter.predicate == .init(operator: .contains,
- bindableValue: .value("ios"),
+ bindableValue: .value("[App.Package.PlatformCompatibility.iOS]"),
displayValue: "iOS"))
// test view representation
@@ -421,7 +404,7 @@ extension AllTests.SearchFilterTests {
value: "executable"))
#expect(filter.key == .productType)
#expect(filter.predicate == .init(operator: .contains,
- bindableValue: .value("executable"),
+ bindableValue: .value("[App.ProductTypeSearchFilter.ProductType.executable]"),
displayValue: "Executable"))
// test view representation
@@ -440,7 +423,7 @@ extension AllTests.SearchFilterTests {
let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, value: "macro"))
#expect(filter.key == .productType)
#expect(filter.predicate == .init(operator: .contains,
- bindableValue: .value("macro"),
+ bindableValue: .value("[App.ProductTypeSearchFilter.ProductType.macro]"),
displayValue: "Macro"))
// test view representation
@@ -530,9 +513,19 @@ extension App.SearchFilter.Predicate: Swift.Equatable {
extension App.SearchFilter.Predicate.BoundValue: Swift.Equatable {
public static func == (lhs: SearchFilter.Predicate.BoundValue, rhs: SearchFilter.Predicate.BoundValue) -> Bool {
- renderGenericSQL(lhs.sqlBind) == renderGenericSQL(rhs.sqlBind)
+ switch (lhs, rhs) {
+ case (.value(let l), .value(let r)):
+ return "\(l)" == "\(r)"
+ case (.array(let l), .array(let r)):
+ return "\(l)" == "\(r)"
+ default:
+ return false
+ }
}
+}
+
+extension App.SearchFilter.Predicate.BoundValue {
var asPlatforms: [Package.PlatformCompatibility]? {
switch self {
case .value(let value):
@@ -543,13 +536,3 @@ extension App.SearchFilter.Predicate.BoundValue: Swift.Equatable {
}
}
}
-
-
-// This renderSQL helper uses a dummy SQLDatabase dialect defined in `TestDatabase`.
-// It should only be used in cases where app.db (which is using the PostgresDB dialect)
-// is not available and where the exact syntax of SQL details is not relevant.
-private func renderGenericSQL(_ query: SQLExpression) -> String {
- var serializer = SQLSerializer(database: TestDatabase())
- query.serialize(to: &serializer)
- return serializer.sql
-}
diff --git a/Tests/AppTests/SearchTests.swift b/Tests/AppTests/SearchTests.swift
index faa64a3cf..72ceda26d 100644
--- a/Tests/AppTests/SearchTests.swift
+++ b/Tests/AppTests/SearchTests.swift
@@ -1331,7 +1331,7 @@ extension AllTests.SearchTests {
do {
// MUT
- let res = try await Search.fetch(app.db, ["test", "license:compatible"], page: 1, pageSize: 20)
+ let res = try await Search.fetch(app.db, ["test", "license:known"], page: 1, pageSize: 20)
// validate
#expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"])
diff --git a/Tests/AppTests/WebpageSnapshotTests.swift b/Tests/AppTests/WebpageSnapshotTests.swift
index a7f4f4c01..03512bcaf 100644
--- a/Tests/AppTests/WebpageSnapshotTests.swift
+++ b/Tests/AppTests/WebpageSnapshotTests.swift
@@ -145,7 +145,7 @@ extension AllTests.WebpageSnapshotTests {
assertSnapshot(of: page, as: .html)
}
- @Test func PackageShow_document_open_source_license() throws {
+ @Test func PackageShow_document_known_license() throws {
var model = API.PackageController.GetRoute.Model.mock
model.license = .mit
model.licenseUrl = "https://example.com/license.html"
@@ -154,18 +154,9 @@ extension AllTests.WebpageSnapshotTests {
assertSnapshot(of: page, as: .html)
}
- @Test func PackageShow_document_app_store_incompatible_license() throws {
+ @Test func PackageShow_document_unknown_license() throws {
var model = API.PackageController.GetRoute.Model.mock
- model.license = .gpl_3_0
- model.licenseUrl = "https://example.com/license.html"
-
- let page = { PackageShow.View(path: "", model: model, packageSchema: .mock).document() }
- assertSnapshot(of: page, as: .html)
- }
-
- @Test func PackageShow_document_other_license() throws {
- var model = API.PackageController.GetRoute.Model.mock
- model.license = .other
+ model.license = .unknown
model.licenseUrl = "https://example.com/license.html"
let page = { PackageShow.View(path: "", model: model, packageSchema: .mock).document() }
diff --git a/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/MaintainerInfoIndex_document.1.html b/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/MaintainerInfoIndex_document.1.html
index 418e990a7..4d7b540ec 100644
--- a/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/MaintainerInfoIndex_document.1.html
+++ b/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/MaintainerInfoIndex_document.1.html
@@ -183,7 +183,7 @@
Package Score
License
10 points
-
OSI-compatible license which is compatible with the App Store.
When working with a Swift Package Manager manifest:
-
Select a package version:
-
-
- 5.2.0
-
-
-
-
-
- 5.3.0-beta.1
-
-
-
-
-
- main
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque quis porttitor erat. Vivamus porttitor mi odio, quis imperdiet velit blandit id. Vivamus vehicula urna eget ipsum laoreet, sed porttitor sapien malesuada. Mauris faucibus tellus at augue vehicula, vitae aliquet felis ullamcorper. Praesent vitae leo rhoncus, egestas elit id, porttitor lacus. Cras ac bibendum mauris. Praesent luctus quis nulla sit amet tempus. Ut pharetra non augue sed pellentesque.
-
-
-
-
-
Written by Author One, Author Two, Author Three, and 5 other contributors.
-
- main
-
- Default Branch
- Modified 12 minutes ago
-
-
-
-
-
-
- Do you maintain this package? Get shields.io compatibility badges and learn how to control our build system.
- Learn more.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Package ID
-
CAFECAFE-CAFE-CAFE-CAFE-CAFECAFECAFE
-
-
-
Score
-
10
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_open_source_license.1.html b/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_known_license.1.html
similarity index 100%
rename from Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_open_source_license.1.html
rename to Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_known_license.1.html
diff --git a/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_no_license.1.html b/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_no_license.1.html
index 4ba10b772..8606362f3 100644
--- a/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_no_license.1.html
+++ b/Tests/AppTests/__Snapshots__/WebpageSnapshotTests/PackageShow_document_no_license.1.html
@@ -163,10 +163,7 @@
When working with a Swift Package Manager manifest: