diff --git a/app.psgi b/app.psgi index 5eb85d73118..5847ac19738 100644 --- a/app.psgi +++ b/app.psgi @@ -145,6 +145,7 @@ my @js_files = map {"/static/js/$_.js"} ( bootstrap/bootstrap-tooltip bootstrap/bootstrap-affix bootstrap-slidepanel + syntaxhighlighter ), ); my @css_files diff --git a/lib/MetaCPAN/Web/Controller/Pod.pm b/lib/MetaCPAN/Web/Controller/Pod.pm index 26015d3c9dd..e659e8c7ae6 100644 --- a/lib/MetaCPAN/Web/Controller/Pod.pm +++ b/lib/MetaCPAN/Web/Controller/Pod.pm @@ -109,7 +109,7 @@ sub view : Private { br => [], caption => [], center => [], - code => [], + code => [ { class => qr/^language-\S+$/ } ], dd => ['id'], div => [qw(id style)], dl => ['id'], @@ -125,18 +125,25 @@ sub view : Private { img => [qw( alt border height width src style title / )], li => ['id'], ol => [], - p => [qw(class style)], - pre => [qw(id class style)], - span => [qw(style)], - strong => [], - sub => [], - sup => [], - table => [qw( style class border cellspacing cellpadding align )], - tbody => [], - td => [qw(style class)], - tr => [qw(style class)], - u => [], - ul => ['id'], + p => [qw(style)], + pre => [ + qw(id style), + { + class => qr/^line-numbers$/, + 'data-line' => qr/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/, + 'data-start' => qr/^\d+$/, + } + ], + span => [qw(style)], + strong => [], + sub => [], + sup => [], + table => [qw( style border cellspacing cellpadding align )], + tbody => [], + td => [qw(style)], + tr => [qw(style)], + u => [], + ul => ['id'], } ); diff --git a/lib/MetaCPAN/Web/Controller/Source.pm b/lib/MetaCPAN/Web/Controller/Source.pm index 31c90cc60e4..dcbe5371a2e 100644 --- a/lib/MetaCPAN/Web/Controller/Source.pm +++ b/lib/MetaCPAN/Web/Controller/Source.pm @@ -79,13 +79,13 @@ sub detect_filetype { local $_ = $file->{path}; # No separate pod brush as of 2011-08-04. - return 'pl' if /\. ( p[ml] | psgi | pod ) $/ix; + return 'perl' if /\. ( p[ml] | psgi | pod ) $/ix; - return 'pl' if /^ cpanfile $/ix; + return 'perl' if /^ cpanfile $/ix; return 'yaml' if /\. ya?ml $/ix; - return 'js' if /\. js(on)? $/ix; + return 'javascript' if /\. js(on)? $/ix; return 'c' if /\. ( c | h | xs ) $/ix; @@ -97,7 +97,7 @@ sub detect_filetype { if ( defined( $file->{mime} ) ) { local $_ = $file->{mime}; - return 'pl' if /perl/; + return 'perl' if /perl/; } # Default to plain text. diff --git a/root/diff.html b/root/diff.html index 5ca005f717d..2269257e2c4 100644 --- a/root/diff.html +++ b/root/diff.html @@ -73,7 +73,7 @@
<% file.path %>
-
<% parts = file.diff.split("\n"); WHILE parts; line = parts.shift; LAST IF line.match( '^\+' ); END; parts.join("\n") %>
+
<% parts = file.diff.split("\n"); WHILE parts; line = parts.shift; LAST IF line.match( '^\+' ); END; parts.join("\n") %>
<% END %> diff --git a/root/pod.html b/root/pod.html index 5d37713dc16..8e653e69c49 100644 --- a/root/pod.html +++ b/root/pod.html @@ -52,7 +52,7 @@
<% IF pod %> -<% pod.replace(/
/, '
').replace(/<\/code><\/pre>/, '
') | none %> +<% pod | none %> <% ELSIF pod_error %>

Error rendering POD for <% module.name %> - <% pod_error %>

<% ELSE %> diff --git a/root/source.html b/root/source.html index 5323edb3b25..c6b43017e79 100644 --- a/root/source.html +++ b/root/source.html @@ -16,36 +16,36 @@
  • - Release Info + Release Info
  • <% IF module.documentation %>
  • - Module Documentation + Module Documentation
  • <% ELSIF module.slop %>
  • - Documentation View + Documentation View
  • <% END %>
  •  
  • -
  • +
  • <% IF module.sloc > 0 %> -
  • +
  • <% END %>
  • <% module.sloc %> lines of code
  • @@ -55,21 +55,10 @@
    <% IF !module.binary %> -
    <% source %>
    +
    " data-pod-lines="<%
    +  module.pod_lines.map(->(lines){ lines.0+1 _ "-" _ (lines.0+lines.1) }).join(', ')
    +%>"><% source %>
    <% ELSE %> This file cannot be displayed inline. Try the raw file. <% END %>
    - - diff --git a/root/static/less/SyntaxHighlighter/shCore.css b/root/static/css/shCore.css similarity index 100% rename from root/static/less/SyntaxHighlighter/shCore.css rename to root/static/css/shCore.css diff --git a/root/static/less/SyntaxHighlighter/shThemeDefault.css b/root/static/css/shThemeDefault.css similarity index 100% rename from root/static/less/SyntaxHighlighter/shThemeDefault.css rename to root/static/css/shThemeDefault.css diff --git a/root/static/js/cpan.js b/root/static/js/cpan.js index 94c32d1d72e..5861260b7a0 100644 --- a/root/static/js/cpan.js +++ b/root/static/js/cpan.js @@ -28,26 +28,6 @@ $.extend({ } }); -var podVisible = false; - -function togglePod(lines) { - var toggle = podVisible ? 'none' : 'block'; - podVisible = !podVisible; - if (!lines || !lines.length) return; - for (var i = 0; i < lines.length; i++) { - var start = lines[i][0], - length = lines[i][1]; - var sourceC = $('.container')[0].children; - var linesC = $('.gutter')[0].children; - var x; - for (x = start; x < start + length; x++) { - sourceC[x].style.display = toggle; - linesC[x].style.display = toggle; - } - - } -} - function togglePanel(side) { var panel = $('#' + side + '-panel'); var shower = $('#show-' + side + '-panel'); @@ -83,95 +63,6 @@ function toggleTOC() { $(document).ready(function () { $(".ttip").tooltip(); - SyntaxHighlighter.defaults['quick-code'] = false; - SyntaxHighlighter.defaults['tab-size'] = 8; - - // Allow tilde in url (#1118). Orig: /\w+:\/\/[\w-.\/?%&=:@;#]*/g, - SyntaxHighlighter.regexLib['url'] = /\w+:\/\/[\w-.\/?%&=:@;#~]*/g; - - /** - * Turns all package names into metacpan.org links within tags. - * @param {String} code Input code. - * @return {String} Returns code with tags. - */ - function processPackages(code) - { - var destination = document.location.href.match(/\/source\//) ? 'source' : 'pod', - strip_delimiters = /((?:q[qw]?)?.)([A-Za-z0-9\:]+)(.*)/ - ; - - code = code.replace(/((?:with|extends|use<\/code> (?:parent|base|aliased))\s*<\/code>\s*)(.+?)(<\/code>)/g, function(m,prefix,pkg,suffix) - { - var match = null, - mcpan_url - ; - - if ( match = strip_delimiters.exec(pkg) ) - { - prefix = prefix + match[1]; - pkg = match[2]; - suffix = match[3] + suffix; - } - - mcpan_url = '' + pkg + ''; - return prefix + mcpan_url + suffix; - }); - - // Link our dependencies - return code.replace(/((use|package|require)<\/code> )([A-Za-z0-9\:]+)(.*?<\/code>)/g, '$1$3$4'); - }; - - var getCodeLinesHtml = SyntaxHighlighter.Highlighter.prototype.getCodeLinesHtml; - SyntaxHighlighter.Highlighter.prototype.getCodeLinesHtml = function(html, lineNumbers) { - html = html.replace(/^ /, " "); - html = getCodeLinesHtml.call(this, html, lineNumbers); - return processPackages(html); - }; - - var getLineNumbersHtml = SyntaxHighlighter.Highlighter.prototype.getLineNumbersHtml; - SyntaxHighlighter.Highlighter.prototype.getLineNumbersHtml = function() { - var html = getLineNumbersHtml.apply(this, arguments); - html = html.replace(/(]*>\s*)(\d+)(\s*<\/div>)/g, '$1$2$3'); - return html; - }; - - - var source = $("#source"); - // if this is a source-code view with destination anchor - if (source.length && source.html().length > 500000) { - source.removeClass(); - } - else if (source[0] && document.location.hash) { - // check for 'L{number}' anchor in URL and highlight and jump - // to that line. - var lineMatch = document.location.hash.match(/^#L(\d+)$/); - if (lineMatch) { - SyntaxHighlighter.defaults['highlight'] = [lineMatch[1]]; - } - else { - // check for 'P{encoded_package_name}' anchor, convert to - // line number (if possible), and then highlight and jump - // as long as the matching line is not the first line in - // the code. - var packageMatch = document.location.hash.match(/^#P(\S+)$/); - if (packageMatch) { - var decodedPackageMatch = decodeURIComponent(packageMatch[1]); - var leadingSource = source.html().split("package " + decodedPackageMatch + ";"); - var lineCount = leadingSource[0].split("\n").length; - if (leadingSource.length > 1 && lineCount > 1) { - SyntaxHighlighter.defaults['highlight'] = [lineCount]; - document.location.hash = "#L" + lineCount; - } - else { - // reset the anchor portion of the URL (it just looks neater). - document.location.hash = ''; - } - } - } - } - - SyntaxHighlighter.highlight(); - $('#signin-button').mouseenter(function () { $('#signin').show() }); $('#signin').mouseleave(function () { $('#signin').hide() }); diff --git a/root/static/js/syntaxhighlighter.js b/root/static/js/syntaxhighlighter.js new file mode 100644 index 00000000000..3f83d27eb8b --- /dev/null +++ b/root/static/js/syntaxhighlighter.js @@ -0,0 +1,203 @@ +$(function () { + // convert a string like "1,3-5,7" into an array [1,3,4,5,7] + function parseLines (lines) { + lines = lines.split(/\s*,\s*/); + var all_lines = []; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + var res = line.match(/^\s*(\d+)\s*(?:-\s*(\d+)\s*)?$/); + if (res) { + var start = res[1]*1; + var end = (res[2] || res[1])*1; + for (var l = start; l <= end; l++) { + all_lines.push(l); + } + } + } + return all_lines; + } + + function findLines (el, lines) { + var selector = $.map( + parseLines(lines), + function (i, line) { return '.number' + line } + ).join(', '); + return el.find('.syntaxhighlighter .line').filter(selector); + } + + var hashLines = /^#L(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)$/; + + // Allow tilde in url (#1118). Orig: /\w+:\/\/[\w-.\/?%&=:@;#]*/g, + SyntaxHighlighter.regexLib['url'] = /\w+:\/\/[\w-.\/?%&=:@;#~]*/g; + + /** + * Turns all package names into metacpan.org links within tags. + * @param {String} code Input code. + * @return {String} Returns code with tags. + */ + function processPackages(code) + { + var destination = document.location.href.match(/\/source\//) ? 'source' : 'pod', + strip_delimiters = /((?:q[qw]?)?.)([A-Za-z0-9\:]+)(.*)/ + ; + + code = code.replace(/((?:with|extends|use<\/code> (?:parent|base|aliased))\s*<\/code>\s*)(.+?)(<\/code>)/g, function(m,prefix,pkg,suffix) + { + var match = null, + mcpan_url + ; + + if ( match = strip_delimiters.exec(pkg) ) + { + prefix = prefix + match[1]; + pkg = match[2]; + suffix = match[3] + suffix; + } + + mcpan_url = '' + pkg + ''; + return prefix + mcpan_url + suffix; + }); + + // Link our dependencies + return code.replace(/((use|package|require)<\/code> )([A-Z_a-z][0-9A-Z_a-z]*(?:::[0-9A-Z_a-z]+)*)(.*?<\/code>)/g, '$1$3$4'); + }; + + var getCodeLinesHtml = SyntaxHighlighter.Highlighter.prototype.getCodeLinesHtml; + SyntaxHighlighter.Highlighter.prototype.getCodeLinesHtml = function(html, lineNumbers) { + // the syntax highlighter has a bug that strips spaces from the first line. + // replace any leading whitespace with an entity, preventing that. + html = html.replace(/^ /, " "); + html = html.replace(/^\t/, " "); + html = getCodeLinesHtml.call(this, html, lineNumbers); + return processPackages(html); + }; + + + var source = $("#source"); + if (source.length) { + var lineMatch; + var packageMatch; + // avoid highlighting excessively large blocks of code as they will take + // too long, causing browsers to lag and offer to kill the script + if (source.html().length > 500000) { + source.children('code').removeClass(); + } + // save highlighted lines in an attribute, to be used later + else if ( lineMatch = document.location.hash.match(hashLines) ) { + source.attr('data-line', lineMatch[1]); + } + // check for 'P{encoded_package_name}' anchor, convert to + // line number (if possible), and then highlight and jump + // as long as the matching line is not the first line in + // the code. + else if ( packageMatch = document.location.hash.match(/^#P(\S+)$/) ) { + var decodedPackageMatch = decodeURIComponent(packageMatch[1]); + var leadingSource = source.text().split("package " + decodedPackageMatch + ";"); + var lineCount = leadingSource[0].split("\n").length; + if (leadingSource.length > 1 && lineCount > 1) { + source.attr('data-line', lineCount); + document.location.hash = "#L" + lineCount; + } + else { + // reset the anchor portion of the URL (it just looks neater). + document.location.hash = ''; + } + } + } + + // on pod pages, set the language to perl if no other language is set + $(".pod pre > code").each(function(index, code) { + var have_lang; + if (code.className && code.className.match(/(?:\s|^)language-\S+/)) { + return; + } + $(code).addClass('language-perl'); + }); + + $(".content pre > code").each(function(index, code) { + var pre = $(code).parent(); + + var config = { + 'gutter' : false, + 'toolbar' : false, + 'quick-code' : false, + 'tab-size' : 8 + }; + if (code.className) { + var res = code.className.match(/(?:\s|^)language-(\S+)/); + if (res) { + config.brush = res[1]; + } + } + if (!config.brush) { + return; + } + + if (pre.hasClass('line-numbers')) { + config.gutter = true; + } + // starting line number can be provided by an attribute + var first_line = pre.attr('data-start'); + if (first_line) { + config['first-line'] = first_line; + } + // highlighted lines can be provided by an attribute + var lines = pre.attr('data-line'); + if (lines) { + config.highlight = parseLines(lines); + } + + SyntaxHighlighter.highlight(config, code); + + var pod_lines = pre.attr('data-pod-lines'); + if (pod_lines) { + findLines(pre, pod_lines).addClass('pod-line'); + } + }); + + if (source.length) { + // on the source page, make line numbers into links + source.find('.syntaxhighlighter .gutter .line').each(function(i, el) { + var line = $(el); + var res; + if (res = line.attr('class').match(/(^|\s)number(\d+)(\s|$)/)) { + var linenr = res[2]; + var id = 'L' + linenr; + line.contents().wrap(''); + var link = line.children('a'); + link.click(function(e) { + // normally the browser would update the url and scroll to + // the the link. instead, update the hash ourselves, but + // unset the id first so it doesn't scroll + e.preventDefault(); + link.removeAttr('id'); + document.location.hash = '#' + id; + link.attr('id', id); + }); + } + }); + + // the line ids are added by javascript, so the browser won't have + // scrolled to it. also, highlight ranges don't correspond to exact + // ids. do the initial scroll ourselves. + var res; + if (res = document.location.hash.match(/^(#L\d+)(-|,|$)/)) { + var el = $(res[1]); + $('html, body').scrollTop(el.offset().top); + } + + // if someone changes the url hash manually, update the highlighted lines + $(window).on('hashchange', function() { + var lineMatch; + if (lineMatch = document.location.hash.match(hashLines) ) { + source.attr('data-line', lineMatch[1]); + source.find('.highlighted').removeClass('highlighted'); + findLines(source, lineMatch[1]).addClass('highlighted'); + } + }); + } +}); + +function togglePod() { + $('.pod-toggle').toggleClass('pod-hidden'); +} diff --git a/root/static/less/global.less b/root/static/less/global.less index 140966cc916..4c4dc845544 100644 --- a/root/static/less/global.less +++ b/root/static/less/global.less @@ -245,7 +245,6 @@ ul { /* Contributors list on release pages * see /release/Plack for example */ - #contributors { min-height: 40px; diff --git a/root/static/less/pod.less b/root/static/less/pod.less index 33a185db271..f8fc4601db5 100644 --- a/root/static/less/pod.less +++ b/root/static/less/pod.less @@ -133,9 +133,6 @@ ul#index, #index ul { } } -.nogutter, .pod pre { - padding-left: 10px; -} .pod p.pod-error { border-left: 1px solid #f32; margin-left: -16px; diff --git a/root/static/less/syntaxhighlighter.less b/root/static/less/syntaxhighlighter.less index 86ccf52228a..48b981a8d9e 100644 --- a/root/static/less/syntaxhighlighter.less +++ b/root/static/less/syntaxhighlighter.less @@ -1,20 +1,10 @@ -@import (less) "SyntaxHighlighter/shCore.css"; -@import (less) "SyntaxHighlighter/shThemeDefault.css"; - body .syntaxhighlighter { - font-size: 90% !important; - *, * *, * * * { - font-family: @font-family-monospace !important; - } -} - -.syntaxhighlighter { - border: 1px solid #e9e9e9; - width: auto !important; - overflow-y: hidden !important; - background-color: #fafafa; - padding: 10px; - -webkit-text-size-adjust: 100%; /* for iPhone , issue #107 */ + font-size: 100% !important; + margin: 0 !important; + /* needs higher specificity than the syntax highligher's rules */ + &, *, * *, * * *, * * * * { + font-family: inherit !important; + } } /* work around incompatibility between bootstrap and syntaxhighlighter @@ -24,3 +14,18 @@ body .syntaxhighlighter { content: none !important; } +.pod-hidden { + .pod-line { + display: none; + } + .hide-pod { + display: none; + } + .show-pod { + display: inline; + } +} + +.show-pod { + display: none; +} diff --git a/t/controller/pod.t b/t/controller/pod.t index 1c1d6045d86..79e36280a99 100644 --- a/t/controller/pod.t +++ b/t/controller/pod.t @@ -31,10 +31,6 @@ test_psgi app, sub { 'content of both urls is exactly the same' ); - like $tx->find_value('//div[contains(@class, "pod")]//pre/@class'), - qr/^brush: pl; .+; metacpan-verbatim$/, - 'verbatim pre tag has syn-hi class'; - # Request with lowercase author redirects to uppercase author. ( my $lc_this = $this ) =~ s{(/pod/release/)([^/]+)}{$1\L$2}; # lc author name diff --git a/t/controller/source.t b/t/controller/source.t index 11f03a82dae..3b1456c3d92 100644 --- a/t/controller/source.t +++ b/t/controller/source.t @@ -30,10 +30,9 @@ test_psgi app, sub { ok( my $res = $cb->( GET $uri ), "GET $uri" ); is( $res->code, 200, 'code 200' ); my $tx = tx($res); - ok( - my $source = $tx->find_value( - qq{//div[\@class="content"]/pre[starts-with(\@class, "brush: $type; ")]} - ), + like( + $tx->find_value(q{//div[@class="content"]/pre/code/@class}), + qr/\blanguage-perl\b/, 'has pre-block with expected syntax brush' ); } @@ -44,12 +43,12 @@ test_psgi app, sub { # Test filetype detection. This is based on file attributes so we don't # need to do the API hits to test each type. my @tests = ( - [ pl => 'lib/Template/Manual.pod' ], # pod - [ pl => 'lib/Dist/Zilla.pm' ], - [ pl => 'Makefile.PL' ], + [ perl => 'lib/Template/Manual.pod' ], # pod + [ perl => 'lib/Dist/Zilla.pm' ], + [ perl => 'Makefile.PL' ], - [ js => 'META.json' ], - [ js => 'script.js' ], + [ javascript => 'META.json' ], + [ javascript => 'script.js' ], [ yaml => 'META.yml' ], [ yaml => 'config.yaml' ], @@ -60,11 +59,11 @@ test_psgi app, sub { [ cpanchanges => 'Changes' ], - [ pl => { path => 'bin/dzil', mime => 'text/x-script.perl' } ], + [ perl => { path => 'bin/dzil', mime => 'text/x-script.perl' } ], # There wouldn't normally be a file with no path # but that doesn't mean this shouldn't work. - [ pl => { mime => 'text/x-script.perl' } ], + [ perl => { mime => 'text/x-script.perl' } ], [ plain => 'README' ], );