diff --git a/.gitignore b/.gitignore index ac787cc33c76b8381057dcc186f9035e4a1adbd2..92077e16995dd10d83f8a30c671a4068febcd808 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ swh/web/static/js/ swh/web/static/css/ swh/web/static/fonts/ swh/web/static/jssources/ +swh/web/static/img/thirdParty/ .cache-loader/ build/ dist/ diff --git a/package.json b/package.json index 979dbc46526626c361e8843bc211bb2d7bbaab01..ab4c73132ac1b3dc4fab333c073111a744c66803 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "ansi_up": "^4.0.3", "bootstrap": "^4.3.1", "bootstrap-year-calendar-bs4": "^1.0.0", + "chosen-js": "^1.8.7", "clipboard": "^2.0.4", "core-js": "^3.1.4", "d3": "^5.9.7", diff --git a/swh/web/assets/config/webpack.config.development.js b/swh/web/assets/config/webpack.config.development.js index 569a14d77f6d199558a113e0236a0ec88d74cee3..e6c49f54dc255ea01c3d967a8a3d1a58eacc9afd 100644 --- a/swh/web/assets/config/webpack.config.development.js +++ b/swh/web/assets/config/webpack.config.development.js @@ -302,6 +302,15 @@ module.exports = { outputPath: 'fonts/' } }] + }, { + test: /\.png$/, + use: [{ + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'img/thirdParty/' + } + }] } ], // tell webpack to not parse minified pdfjs file to speedup build process diff --git a/swh/web/assets/src/bundles/browse/content.css b/swh/web/assets/src/bundles/browse/content.css index adcaf49a0716b05664cd4eecefa1c694e51a881b..58f29e81e9a81a971a59b463087825d93a500163 100644 --- a/swh/web/assets/src/bundles/browse/content.css +++ b/swh/web/assets/src/bundles/browse/content.css @@ -21,3 +21,32 @@ .swh-content svg { max-width: 100%; } + +.chosen-container { + color: #444; +} + +.chosen-container .chosen-single div b { + margin-top: 2px; + filter: brightness(0%); +} + +.chosen-container .chosen-single { + height: 31px; + line-height: 1.5; + border-radius: 0px; + background-color: #f4f4f4; + border-color: #ddd; + background-image:none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + font-weight: 400; + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; +} + +.chosen-container .chosen-single:hover { + background-color: #e7e7e7; +} diff --git a/swh/web/assets/src/bundles/browse/origin-search.js b/swh/web/assets/src/bundles/browse/origin-search.js index 78a9bafa3f02c56d4723ef8f34d64f00deae8ff5..c5e68325fcb19aa8e91c6524efe6a703bb14477e 100644 --- a/swh/web/assets/src/bundles/browse/origin-search.js +++ b/swh/web/assets/src/bundles/browse/origin-search.js @@ -43,7 +43,7 @@ function populateOriginSearchResultsTable(origins, offset) { table.append(tableRow); // get async latest visit snapshot and update visit status icon let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url); - latestSnapshotUrl += "?require_snapshot=true"; + latestSnapshotUrl += '?require_snapshot=true'; fetch(latestSnapshotUrl) .then(response => response.json()) .then(data => { diff --git a/swh/web/assets/src/bundles/vendors/index.js b/swh/web/assets/src/bundles/vendors/index.js index 11a3e44a1d21f68b94b2b1afca28d0bd2cd6d253..91fb058ce3528f77bdc29449fc3e7a028b39d8d4 100644 --- a/swh/web/assets/src/bundles/vendors/index.js +++ b/swh/web/assets/src/bundles/vendors/index.js @@ -31,6 +31,12 @@ import 'datatables.net-bs4/css/dataTables.bootstrap4.css'; import 'datatables.net-responsive-bs4/css/responsive.bootstrap4.css'; import './datatables.css'; +// chosen-js +import 'chosen-js'; +import 'chosen-js/chosen.min.css'; +import 'chosen-js/chosen-sprite.png'; +import 'chosen-js/chosen-sprite@2x.png'; + // iframe-resizer import 'iframe-resizer'; diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py index d91f9d5843fd7e62f1a39b9349720dd34f0dce75..4fd9361bec162023e9efb0a6aacb5034c055c320 100644 --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -14,7 +14,7 @@ from django.template.defaultfilters import filesizeformat from swh.model.hashutil import hash_to_hex -from swh.web.common import query, service +from swh.web.common import query, service, highlightjs from swh.web.common.utils import ( reverse, gen_path_info, swh_object_icons ) @@ -181,6 +181,8 @@ def content_display(request, query_string): raise_if_unavailable=False) origin_type = request.GET.get('origin_type', None) origin_url = request.GET.get('origin_url', None) + selected_language = request.GET.get('language', None) + if not origin_url: origin_url = request.GET.get('origin', None) snapshot_context = None @@ -218,6 +220,15 @@ def content_display(request, query_string): language = content_display_data['language'] mimetype = content_display_data['mimetype'] + # Override language with user-selected language + if selected_language is not None: + language = selected_language + + available_languages = None + + if mimetype and 'text/' in mimetype: + available_languages = highlightjs._hljs_languages + root_dir = None filename = None path_info = None @@ -303,6 +314,7 @@ def content_display(request, query_string): 'max_content_size': content_display_max_size, 'mimetype': mimetype, 'language': language, + 'available_languages': available_languages, 'breadcrumbs': breadcrumbs, 'top_right_link': { 'url': content_raw_url, diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index 7a55a06ab1988e0acdff393eeef4f423aa3c2b02..b03cb0e28fdddbeb9e0a8f55f26a64e6c539ebf5 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -79,9 +79,10 @@ def origin_content_browse(request, origin_url, origin_type=None, path=None, * :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/content/(path)/` """ # noqa + language = request.GET.get('language', None) return browse_snapshot_content(request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp, - path=path) + path=path, selected_language=language) PER_PAGE = 20 diff --git a/swh/web/browse/views/snapshot.py b/swh/web/browse/views/snapshot.py index 25be99327bd0eb8e75080296358d3f88049ac347..e40a50963f7e413938c8a57d4a04206613ba6cc2 100644 --- a/swh/web/browse/views/snapshot.py +++ b/swh/web/browse/views/snapshot.py @@ -60,7 +60,9 @@ def snapshot_content_browse(request, snapshot_id, path): The url that points to it is :http:get:`/browse/snapshot/(snapshot_id)/content/(path)/` """ - return browse_snapshot_content(request, snapshot_id=snapshot_id, path=path) + language = request.GET.get('language', None) + return browse_snapshot_content(request, snapshot_id=snapshot_id, path=path, + selected_language=language) @browse_route(r'snapshot/(?P<snapshot_id>[0-9a-f]+)/log/', diff --git a/swh/web/browse/views/utils/snapshot_context.py b/swh/web/browse/views/utils/snapshot_context.py index 60e8180af0074387e334f56b856917f1ad441363..3a22f80375a14a7a976e222e7e0db020f262930f 100644 --- a/swh/web/browse/views/utils/snapshot_context.py +++ b/swh/web/browse/views/utils/snapshot_context.py @@ -23,7 +23,7 @@ from swh.web.browse.utils import ( gen_snapshot_link, process_snapshot_branches ) -from swh.web.common import service +from swh.web.common import service, highlightjs from swh.web.common.exc import ( handle_view_exception, NotFoundExc ) @@ -399,7 +399,8 @@ def browse_snapshot_directory(request, snapshot_id=None, origin_type=None, def browse_snapshot_content(request, snapshot_id=None, origin_type=None, - origin_url=None, timestamp=None, path=None): + origin_url=None, timestamp=None, path=None, + selected_language=None): """ Django view implementation for browsing a content in a snapshot context. """ @@ -453,6 +454,15 @@ def browse_snapshot_content(request, snapshot_id=None, origin_type=None, language = content_display_data['language'] mimetype = content_display_data['mimetype'] + # Override language with user-selected language + if selected_language is not None: + language = selected_language + + available_languages = None + + if mimetype and 'text/' in mimetype: + available_languages = highlightjs._hljs_languages + browse_view_name = 'browse-' + swh_type + '-directory' breadcrumbs = [] @@ -562,6 +572,7 @@ def browse_snapshot_content(request, snapshot_id=None, origin_type=None, 'max_content_size': content_display_max_size, 'mimetype': mimetype, 'language': language, + 'available_languages': available_languages, 'breadcrumbs': breadcrumbs if root_sha1_git else [], 'top_right_link': { 'url': content_raw_url, diff --git a/swh/web/templates/includes/content-display.html b/swh/web/templates/includes/content-display.html index cad9f23b6a3b911ce9f87a797f742fca98d3ac55..4a625521546eccae62132b94f937e1f8f933ef41 100644 --- a/swh/web/templates/includes/content-display.html +++ b/swh/web/templates/includes/content-display.html @@ -53,7 +53,25 @@ See top-level LICENSE file for more information {% elif swh_object_metadata.filename and swh_object_metadata.filename|default:""|slice:"-5:" == "ipynb" %} swh.webapp.renderNotebook({{ top_right_link.url|jsonify }}, '.swh-ipynb'); {% elif content %} + let codeContainer = $('code'); + let content = codeContainer.text(); + swh.webapp.highlightCode(); + + function updateLanguage(language) { + codeContainer.text(content); + codeContainer.removeClass(); + codeContainer.addClass(language); + + let urlParams = new URLSearchParams(window.location.search); + urlParams.set('language', language); + + const newUrl = window.location.pathname + '?' + urlParams.toString() + window.location.hash; + window.history.replaceState('', document.title, newUrl); + + swh.webapp.highlightCode(); + } + {% endif %} </script> {% endif %} diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index 94a2a3bb3e35a74c57de582c79e98cc656b6777c..21c2bf9a046f6c2fb6495ca64e813573ac2084c8 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -94,6 +94,14 @@ See top-level LICENSE file for more information {{ top_right_link.text }} </a> {% endif %} + {% if available_languages %} + <select data-placeholder="Select Language" class="language-select chosen-select"> + <option value=""></option> + {% for lang in available_languages %} + <option value="{{ lang }}">{{ lang }}</option> + {% endfor %} + </select> + {% endif %} {% if show_actions_menu %} <button class="btn btn-default btn-sm dropdown-toggle" type="button" data-toggle="dropdown"> <i class="fa fa-bars fa-fw" aria-hidden="true"></i>Actions @@ -122,5 +130,11 @@ See top-level LICENSE file for more information snapshotContext = true; branch = "{{ snapshot_context.branch|escape }}"; {% endif %} + {% if available_languages %} + $(".chosen-select").val("{{ language }}"); + $(".chosen-select").chosen().change(function(event, params) { + updateLanguage(params.selected); + }); + {% endif %} swh.browse.initSnapshotNavigation(snapshotContext, branch !== "None"); </script> diff --git a/yarn.lock b/yarn.lock index c234062f1d0e3efd7f327deacd207bcf437344ca..78b94a756449235af0cf3a865641a0e7bed97736 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2005,6 +2005,11 @@ chokidar@^2.0.2, chokidar@^2.1.5, chokidar@^2.1.6: optionalDependencies: fsevents "^1.2.7" +chosen-js@^1.8.7: + version "1.8.7" + resolved "https://registry.yarnpkg.com/chosen-js/-/chosen-js-1.8.7.tgz#9bfa5597f5081d602ff4ae904af9aef33265bb1d" + integrity sha512-eVdrZJ2U5ISdObkgsi0od5vIJdLwq1P1Xa/Vj/mgxkMZf14DlgobfB6nrlFi3kW4kkvKLsKk4NDqZj1MU1DCpw== + chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"