<% 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/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 @@
-<% 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 @@
/, '').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 @@
Tools
<% IF module.documentation %>
<% ELSIF module.slop %>
<% END %>
-
+
<% IF module.sloc > 0 %>
-
+
<% END %>
Info
<% 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' ],
);