Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation clamp fixes #4858

Merged
merged 1 commit into from
Oct 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/NuGetGallery/App_Start/AppActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ private static void BundlingPostStart()
BundleTable.Bundles.Add(homeScriptBundle);

var displayPackageScriptBundle = new ScriptBundle("~/Scripts/gallery/page-display-package.min.js")
.Include("~/Scripts/gallery/page-display-package.js");
.Include("~/Scripts/gallery/page-display-package.js")
.Include("~/Scripts/gallery/clamp.js");
BundleTable.Bundles.Add(displayPackageScriptBundle);

var managePackagesScriptBundle = new ScriptBundle("~/Scripts/gallery/page-manage-packages.min.js")
Expand Down
2 changes: 1 addition & 1 deletion src/NuGetGallery/Controllers/PackagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ public virtual async Task<ActionResult> DisplayPackage(string id, string version
}
}

await _readMeService.GetReadMeHtmlAsync(package, model, isReadMePending);
model.ReadMeHtml = await _readMeService.GetReadMeHtmlAsync(package, isReadMePending);

model.PolicyMessage = GetDisplayPackagePolicyMessage(package.PackageRegistration);

Expand Down
271 changes: 271 additions & 0 deletions src/NuGetGallery/Scripts/gallery/clamp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*!
* Clamp.js 0.7.0
*
* Copyright 2011-2013, Joseph Schmitt http://joe.sh
* Released under the WTFPL license
* http://sam.zoy.org/wtfpl/
*/

(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory();
} else {
// Browser globals
root.$clamp = factory();
}
}(this, function() {
/**
* Clamps a text node.
* @param {HTMLElement} element. Element containing the text node to clamp.
* @param {Object} options. Options to pass to the clamper.
*/
function clamp(element, options) {
options = options || {};

var self = this,
win = window,
opt = {
clamp: options.clamp || 2,
useNativeClamp: typeof(options.useNativeClamp) != 'undefined' ? options.useNativeClamp : true,
splitOnChars: options.splitOnChars || ['.', '-', '–', '—', ' '], //Split on sentences (periods), hypens, en-dashes, em-dashes, and words (spaces).
animate: options.animate || false,
truncationChar: options.truncationChar || '…',
truncationHTML: options.truncationHTML
},

sty = element.style,
originalText = element.innerHTML,

supportsNativeClamp = typeof(element.style.webkitLineClamp) != 'undefined',
clampValue = opt.clamp,
isCSSValue = clampValue.indexOf && (clampValue.indexOf('px') > -1 || clampValue.indexOf('em') > -1),
truncationHTMLContainer;

if (opt.truncationHTML) {
truncationHTMLContainer = document.createElement('span');
truncationHTMLContainer.innerHTML = opt.truncationHTML;
}


// UTILITY FUNCTIONS __________________________________________________________

/**
* Return the current style for an element.
* @param {HTMLElement} elem The element to compute.
* @param {string} prop The style property.
* @returns {number}
*/
function computeStyle(elem, prop) {
if (!win.getComputedStyle) {
win.getComputedStyle = function(el, pseudo) {
this.el = el;
this.getPropertyValue = function(prop) {
var re = /(\-([a-z]){1})/g;
if (prop == 'float') prop = 'styleFloat';
if (re.test(prop)) {
prop = prop.replace(re, function() {
return arguments[2].toUpperCase();
});
}
return el.currentStyle && el.currentStyle[prop] ? el.currentStyle[prop] : null;
};
return this;
};
}

return win.getComputedStyle(elem, null).getPropertyValue(prop);
}

/**
* Returns the maximum number of lines of text that should be rendered based
* on the current height of the element and the line-height of the text.
*/
function getMaxLines(height) {
var availHeight = height || element.clientHeight,
lineHeight = getLineHeight(element);

return Math.max(Math.floor(availHeight / lineHeight), 0);
}

/**
* Returns the maximum height a given element should have based on the line-
* height of the text and the given clamp value.
*/
function getMaxHeight(clmp) {
var lineHeight = getLineHeight(element);
return lineHeight * clmp;
}

/**
* Returns the line-height of an element as an integer.
*/
function getLineHeight(elem) {
var lh = computeStyle(elem, 'line-height');
if (lh == 'normal') {
// Normal line heights vary from browser to browser. The spec recommends
// a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff.
lh = parseInt(computeStyle(elem, 'font-size')) * 1.2;
}
return parseInt(lh);
}


// MEAT AND POTATOES (MMMM, POTATOES...) ______________________________________
var splitOnChars = opt.splitOnChars.slice(0),
splitChar = splitOnChars[0],
chunks,
lastChunk;

/**
* Gets an element's last child. That may be another node or a node's contents.
*/
function getLastChild(elem) {
//Current element has children, need to go deeper and get last child as a text node
if (elem.lastChild.children && elem.lastChild.children.length > 0) {
return getLastChild(Array.prototype.slice.call(elem.children).pop());
}
//This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying
else if (!elem.lastChild || !elem.lastChild.nodeValue || elem.lastChild.nodeValue === '' || elem.lastChild.nodeValue == opt.truncationChar) {
elem.lastChild.parentNode.removeChild(elem.lastChild);
return getLastChild(element);
}
//This is the last child we want, return it
else {
return elem.lastChild;
}
}

/**
* Removes one character at a time from the text until its width or
* height is beneath the passed-in max param.
*/
function truncate(target, maxHeight) {
if (!maxHeight) {
return;
}

/**
* Resets global variables.
*/
function reset() {
splitOnChars = opt.splitOnChars.slice(0);
splitChar = splitOnChars[0];
chunks = null;
lastChunk = null;
}

var nodeValue = target.nodeValue.replace(opt.truncationChar, '');

//Grab the next chunks
if (!chunks) {
//If there are more characters to try, grab the next one
if (splitOnChars.length > 0) {
splitChar = splitOnChars.shift();
}
//No characters to chunk by. Go character-by-character
else {
splitChar = '';
}

chunks = nodeValue.split(splitChar);
}

//If there are chunks left to remove, remove the last one and see if
// the nodeValue fits.
if (chunks.length > 1) {
// console.log('chunks', chunks);
lastChunk = chunks.pop();
// console.log('lastChunk', lastChunk);
applyEllipsis(target, chunks.join(splitChar));
}
//No more chunks can be removed using this character
else {
chunks = null;
}

//Insert the custom HTML before the truncation character
if (truncationHTMLContainer) {
target.nodeValue = target.nodeValue.replace(opt.truncationChar, '');
element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + opt.truncationChar;
}

//Search produced valid chunks
if (chunks) {
//It fits
if (element.clientHeight <= maxHeight) {
//There's still more characters to try splitting on, not quite done yet
if (splitOnChars.length >= 0 && splitChar !== '') {
applyEllipsis(target, chunks.join(splitChar) + splitChar + lastChunk);
chunks = null;
}
//Finished!
else {
return element.innerHTML;
}
}
}
//No valid chunks produced
else {
//No valid chunks even when splitting by letter, time to move
//on to the next node
if (splitChar === '') {
applyEllipsis(target, '');
target = getLastChild(element);

reset();
}
}

//If you get here it means still too big, let's keep truncating
if (opt.animate) {
setTimeout(function() {
truncate(target, maxHeight);
}, opt.animate === true ? 10 : opt.animate);
} else {
return truncate(target, maxHeight);
}
}

function applyEllipsis(elem, str) {
elem.nodeValue = str + opt.truncationChar;
}


// CONSTRUCTOR ________________________________________________________________

if (clampValue == 'auto') {
clampValue = getMaxLines();
} else if (isCSSValue) {
clampValue = getMaxLines(parseInt(clampValue));
}

var clampedText;
if (supportsNativeClamp && opt.useNativeClamp) {
sty.overflow = 'hidden';
sty.textOverflow = 'ellipsis';
sty.webkitBoxOrient = 'vertical';
sty.display = '-webkit-box';
sty.webkitLineClamp = clampValue;

if (isCSSValue) {
sty.height = opt.clamp + 'px';
}
} else {
var height = getMaxHeight(clampValue);
if (height <= element.clientHeight) {
clampedText = truncate(getLastChild(element), height);
}
}

return {
'original': originalText,
'clamped': clampedText
};
}

return clamp;
}));
20 changes: 16 additions & 4 deletions src/NuGetGallery/Scripts/gallery/page-display-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,27 @@ $(function () {
var readmeContainer = $("#readme-container");
if (readmeContainer[0])
{
window.nuget.configureExpanderHeading(
"readme-container");
window.nuget.configureExpanderHeading("readme-container");

window.nuget.configureExpander(
"readme-full",
"readme-more",
"CalculatorAddition",
"Show less",
"CalculatorSubtract",
"Show more");

var showLess = $("#readme-less");
$clamp(showLess[0], { clamp: 10, useNativeClamp: false });

$("#show-readme-more").click(function () {
showLess.collapse("toggle");
});
showLess.on('hide.bs.collapse', function (e) {
e.stopPropagation();
});
showLess.on('show.bs.collapse', function (e) {
e.stopPropagation();
});
}

window.nuget.configureExpanderHeading("dependency-groups");
Expand All @@ -36,7 +48,7 @@ $(function () {
"CalculatorAddition",
"Show less",
"CalculatorSubtract",
"Show more");
"Show more");

for (var i in packageManagers)
{
Expand Down
3 changes: 1 addition & 2 deletions src/NuGetGallery/Services/IReadMeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ public interface IReadMeService
/// Get the converted HTML from the stored ReadMe markdown.
/// </summary>
/// <param name="package">Package entity associated with the ReadMe.</param>
/// <param name="model">Display package view model to populate.</param>
/// <param name="isPending">Whether to retrieve the pending ReadMe.</param>
/// <returns>Pending or active ReadMe converted to HTML.</returns>
Task GetReadMeHtmlAsync(Package package, DisplayPackageViewModel model, bool isPending = false);
Task<string> GetReadMeHtmlAsync(Package package, bool isPending = false);

/// <summary>
/// Get package ReadMe markdown from storage.
Expand Down
17 changes: 5 additions & 12 deletions src/NuGetGallery/Services/ReadMeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal class ReadMeService : IReadMeService
internal const string TypeWritten = "Written";

internal const int MaxMdLengthBytes = 8000;
private const int ReadMeClampedLineCount = 10;
private const string UrlHostRequirement = "raw.githubusercontent.com";

private static readonly TimeSpan UrlTimeout = TimeSpan.FromSeconds(10);
Expand Down Expand Up @@ -80,20 +79,14 @@ public async Task<string> GetReadMeHtmlAsync(ReadMeRequest readMeRequest, Encodi
/// Get the converted HTML from the stored ReadMe markdown.
/// </summary>
/// <param name="package">Package entity associated with the ReadMe.</param>
/// <param name="model">Display package view model to populate.</param>
/// <param name="isPending">Whether to retrieve the pending ReadMe.</param>
/// <returns>Pending or active ReadMe converted to HTML.</returns>
public async Task GetReadMeHtmlAsync(Package package, DisplayPackageViewModel model, bool isPending = false)
public async Task<string> GetReadMeHtmlAsync(Package package, bool isPending = false)
{
var readMeMd = await GetReadMeMdAsync(package, isPending);
if (!string.IsNullOrWhiteSpace(readMeMd))
{
var readMeMdClamped = string.Join(Environment.NewLine,
readMeMd.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Take(ReadMeClampedLineCount));

model.ReadMeHtml = GetReadMeHtml(readMeMd).Trim();
model.ReadMeHtmlClamped = GetReadMeHtml(readMeMdClamped).Trim();
}
return string.IsNullOrEmpty(readMeMd) ?
string.Empty :
GetReadMeHtml(readMeMd);
}

/// <summary>
Expand Down Expand Up @@ -211,7 +204,7 @@ internal static string GetReadMeHtml(string readMeMd)
{
CommonMarkConverter.ProcessStage3(document, htmlWriter, settings);

return CommonMarkLinkPattern.Replace(htmlWriter.ToString(), "$0" + " rel=\"nofollow\"");
return CommonMarkLinkPattern.Replace(htmlWriter.ToString(), "$0" + " rel=\"nofollow\"").Trim();
}
}

Expand Down
1 change: 0 additions & 1 deletion src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public void SetPendingMetadata(PackageEdit pendingMetadata)
public IEnumerable<DisplayPackageViewModel> PackageVersions { get; set; }
public string Copyright { get; set; }
public string ReadMeHtml { get; set; }
public string ReadMeHtmlClamped { get; set; }
public bool HasPendingMetadata { get; private set; }
public bool IsLastEditFailed { get; private set; }
public DateTime? LastEdited { get; set; }
Expand Down
Loading