Update for Shaarli 0.12.2
This commit is contained in:
parent
23a5fc1eef
commit
2c1f0981d9
76 changed files with 9219 additions and 4126 deletions
BIN
assets/myShaarli/fonts/Roboto-Bold.woff
Normal file
BIN
assets/myShaarli/fonts/Roboto-Bold.woff
Normal file
Binary file not shown.
BIN
assets/myShaarli/fonts/Roboto-Bold.woff2
Normal file
BIN
assets/myShaarli/fonts/Roboto-Bold.woff2
Normal file
Binary file not shown.
BIN
assets/myShaarli/fonts/Roboto-Regular.woff
Normal file
BIN
assets/myShaarli/fonts/Roboto-Regular.woff
Normal file
Binary file not shown.
BIN
assets/myShaarli/fonts/Roboto-Regular.woff2
Normal file
BIN
assets/myShaarli/fonts/Roboto-Regular.woff2
Normal file
Binary file not shown.
BIN
assets/myShaarli/img/apple-touch-icon.png
Normal file
BIN
assets/myShaarli/img/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
assets/myShaarli/img/favicon.png
Normal file
BIN
assets/myShaarli/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
assets/myShaarli/img/sad_star.png
Normal file
BIN
assets/myShaarli/img/sad_star.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
718
assets/myShaarli/js/base.js
Normal file
718
assets/myShaarli/js/base.js
Normal file
|
@ -0,0 +1,718 @@
|
|||
import Awesomplete from 'awesomplete';
|
||||
import he from 'he';
|
||||
|
||||
/**
|
||||
* Find a parent element according to its tag and its attributes
|
||||
*
|
||||
* @param element Element where to start the search
|
||||
* @param tagName Expected parent tag name
|
||||
* @param attributes Associative array of expected attributes (name=>value).
|
||||
*
|
||||
* @returns Found element or null.
|
||||
*/
|
||||
function findParent(element, tagName, attributes) {
|
||||
const parentMatch = (key) => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
|
||||
while (element) {
|
||||
if (element.tagName.toLowerCase() === tagName) {
|
||||
if (Object.keys(attributes).find(parentMatch)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax request to refresh the CSRF token.
|
||||
*/
|
||||
function refreshToken(basePath, callback) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `${basePath}/admin/token`);
|
||||
xhr.onload = () => {
|
||||
const elements = document.querySelectorAll('input[name="token"]');
|
||||
[...elements].forEach((element) => {
|
||||
element.setAttribute('value', xhr.responseText);
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback(xhr.response);
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function createAwesompleteInstance(element, separator, tags = []) {
|
||||
const awesome = new Awesomplete(Awesomplete.$(element));
|
||||
|
||||
// Tags are separated by separator. Ignore leading search flags
|
||||
awesome.filter = (text, input) => {
|
||||
let filterFunc = Awesomplete.FILTER_CONTAINS;
|
||||
let term = input.match(new RegExp(`[^${separator}]*$`))[0];
|
||||
const termFlagged = term.replace(/^[-~+]/, '');
|
||||
if (term !== termFlagged) {
|
||||
term = termFlagged;
|
||||
filterFunc = Awesomplete.FILTER_STARTSWITH;
|
||||
}
|
||||
|
||||
return filterFunc(text, term);
|
||||
};
|
||||
|
||||
// Insert new selected tag in the input
|
||||
awesome.replace = (text) => {
|
||||
const before = awesome.input.value.match(new RegExp(`^(.+${separator}+)?[-~+]?|`))[0];
|
||||
awesome.input.value = `${before}${text}${separator}`;
|
||||
};
|
||||
// Highlight found items
|
||||
awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
|
||||
// Don't display already selected items
|
||||
// WARNING: pseudo classes does not seem to work with string litterals...
|
||||
const reg = new RegExp(`([^${separator}]+)${separator}`, 'g');
|
||||
let match;
|
||||
awesome.data = (item, input) => {
|
||||
while ((match = reg.exec(input))) {
|
||||
if (item === match[1]) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return item;
|
||||
};
|
||||
awesome.minChars = 1;
|
||||
if (tags.length) {
|
||||
awesome.list = tags;
|
||||
}
|
||||
|
||||
return awesome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update awesomplete list of tag for all elements matching the given selector
|
||||
*
|
||||
* @param selector CSS selector
|
||||
* @param tags Array of tags
|
||||
* @param instances List of existing awesomplete instances
|
||||
* @param separator Tags separator character
|
||||
*/
|
||||
function updateAwesompleteList(selector, tags, instances, separator) {
|
||||
if (instances.length === 0) {
|
||||
// First load: create Awesomplete instances
|
||||
const elements = document.querySelectorAll(selector);
|
||||
[...elements].forEach((element) => {
|
||||
instances.push(createAwesompleteInstance(element, separator, tags));
|
||||
});
|
||||
} else {
|
||||
// Update awesomplete tag list
|
||||
instances.map((item) => {
|
||||
item.list = tags;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the class 'hidden' to city options not attached to the current selected continent.
|
||||
*
|
||||
* @param cities List of <option> elements
|
||||
* @param currentContinent Current selected continent
|
||||
* @param reset Set to true to reset the selected value
|
||||
*/
|
||||
function hideTimezoneCities(cities, currentContinent, reset = null) {
|
||||
let first = true;
|
||||
if (reset == null) {
|
||||
reset = false;
|
||||
}
|
||||
[...cities].forEach((option) => {
|
||||
if (option.getAttribute('data-continent') !== currentContinent) {
|
||||
option.className = 'hidden';
|
||||
} else {
|
||||
option.className = '';
|
||||
if (reset === true && first === true) {
|
||||
option.setAttribute('selected', 'selected');
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an element up in the tree from its class name.
|
||||
*/
|
||||
function getParentByClass(el, className) {
|
||||
const p = el.parentNode;
|
||||
if (p == null || p.classList.contains(className)) {
|
||||
return p;
|
||||
}
|
||||
return getParentByClass(p, className);
|
||||
}
|
||||
|
||||
function toggleHorizontal() {
|
||||
[...document.getElementById('shaarli-menu').querySelectorAll('.menu-transform')].forEach((el) => {
|
||||
el.classList.toggle('pure-menu-horizontal');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMenu(menu) {
|
||||
// set timeout so that the panel has a chance to roll up
|
||||
// before the menu switches states
|
||||
if (menu.classList.contains('open')) {
|
||||
setTimeout(toggleHorizontal, 500);
|
||||
} else {
|
||||
toggleHorizontal();
|
||||
}
|
||||
menu.classList.toggle('open');
|
||||
document.getElementById('menu-toggle').classList.toggle('x');
|
||||
}
|
||||
|
||||
function closeMenu(menu) {
|
||||
if (menu.classList.contains('open')) {
|
||||
toggleMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFold(button, description, thumb) {
|
||||
// Switch fold/expand - up = fold
|
||||
if (button.classList.contains('fa-chevron-up')) {
|
||||
button.title = document.getElementById('translation-expand').innerHTML;
|
||||
if (description != null) {
|
||||
description.style.display = 'none';
|
||||
}
|
||||
if (thumb != null) {
|
||||
thumb.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
button.title = document.getElementById('translation-fold').innerHTML;
|
||||
if (description != null) {
|
||||
description.style.display = 'block';
|
||||
}
|
||||
if (thumb != null) {
|
||||
thumb.style.display = 'block';
|
||||
}
|
||||
}
|
||||
button.classList.toggle('fa-chevron-down');
|
||||
button.classList.toggle('fa-chevron-up');
|
||||
}
|
||||
|
||||
function removeClass(element, classname) {
|
||||
element.className = element.className.replace(new RegExp(`(?:^|\\s)${classname}(?:\\s|$)`), ' ');
|
||||
}
|
||||
|
||||
function init(description) {
|
||||
function resize() {
|
||||
/* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
|
||||
const scrollTop = window.pageYOffset
|
||||
|| (document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||
|
||||
description.style.height = 'auto';
|
||||
description.style.height = `${description.scrollHeight + 10}px`;
|
||||
|
||||
window.scrollTo(0, scrollTop);
|
||||
}
|
||||
|
||||
/* 0-timeout to get the already changed text */
|
||||
function delayedResize() {
|
||||
window.setTimeout(resize, 0);
|
||||
}
|
||||
|
||||
const observe = (element, event, handler) => {
|
||||
element.addEventListener(event, handler, false);
|
||||
};
|
||||
observe(description, 'change', resize);
|
||||
observe(description, 'cut', delayedResize);
|
||||
observe(description, 'paste', delayedResize);
|
||||
observe(description, 'drop', delayedResize);
|
||||
observe(description, 'keydown', delayedResize);
|
||||
|
||||
resize();
|
||||
}
|
||||
|
||||
(() => {
|
||||
const basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||
const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
|
||||
const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
|
||||
|
||||
/**
|
||||
* Handle responsive menu.
|
||||
* Source: http://purecss.io/layouts/tucked-menu-vertical/
|
||||
*/
|
||||
const menu = document.getElementById('shaarli-menu');
|
||||
const WINDOW_CHANGE_EVENT = ('onorientationchange' in window) ? 'orientationchange' : 'resize';
|
||||
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
if (menuToggle != null) {
|
||||
menuToggle.addEventListener('click', () => toggleMenu(menu));
|
||||
}
|
||||
|
||||
window.addEventListener(WINDOW_CHANGE_EVENT, () => closeMenu(menu));
|
||||
|
||||
/**
|
||||
* Fold/Expand shaares description and thumbnail.
|
||||
*/
|
||||
const foldAllButtons = document.getElementsByClassName('fold-all');
|
||||
const foldButtons = document.getElementsByClassName('fold-button');
|
||||
|
||||
[...foldButtons].forEach((foldButton) => {
|
||||
// Retrieve description
|
||||
let description = null;
|
||||
let thumbnail = null;
|
||||
const linklistItem = getParentByClass(foldButton, 'linklist-item');
|
||||
if (linklistItem != null) {
|
||||
description = linklistItem.querySelector('.linklist-item-description');
|
||||
thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
|
||||
if (description != null || thumbnail != null) {
|
||||
foldButton.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
foldButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
toggleFold(event.target, description, thumbnail);
|
||||
});
|
||||
});
|
||||
|
||||
if (foldAllButtons != null) {
|
||||
[].forEach.call(foldAllButtons, (foldAllButton) => {
|
||||
foldAllButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const state = foldAllButton.firstElementChild.getAttribute('class').indexOf('down') !== -1 ? 'down' : 'up';
|
||||
[].forEach.call(foldButtons, (foldButton) => {
|
||||
if ((foldButton.firstElementChild.classList.contains('fa-chevron-up') && state === 'down')
|
||||
|| (foldButton.firstElementChild.classList.contains('fa-chevron-down') && state === 'up')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Retrieve description
|
||||
let description = null;
|
||||
let thumbnail = null;
|
||||
const linklistItem = getParentByClass(foldButton, 'linklist-item');
|
||||
if (linklistItem != null) {
|
||||
description = linklistItem.querySelector('.linklist-item-description');
|
||||
thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
|
||||
if (description != null || thumbnail != null) {
|
||||
foldButton.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
toggleFold(foldButton.firstElementChild, description, thumbnail);
|
||||
});
|
||||
foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
|
||||
foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
|
||||
foldAllButton.title = state === 'down'
|
||||
? document.getElementById('translation-fold-all').innerHTML
|
||||
: document.getElementById('translation-expand-all').innerHTML;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation message before deletion.
|
||||
*/
|
||||
const deleteLinks = document.querySelectorAll('.confirm-delete');
|
||||
[...deleteLinks].forEach((deleteLink) => {
|
||||
deleteLink.addEventListener('click', (event) => {
|
||||
const type = event.currentTarget.getAttribute('data-type') || 'link';
|
||||
if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Close alerts
|
||||
*/
|
||||
const closeLinks = document.querySelectorAll('.pure-alert-close');
|
||||
[...closeLinks].forEach((closeLink) => {
|
||||
closeLink.addEventListener('click', (event) => {
|
||||
const alert = getParentByClass(event.target, 'pure-alert-closable');
|
||||
alert.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* New version dismiss.
|
||||
* Hide the message for one week using localStorage.
|
||||
*/
|
||||
const newVersionDismiss = document.getElementById('new-version-dismiss');
|
||||
const newVersionMessage = document.querySelector('.new-version-message');
|
||||
if (newVersionMessage != null
|
||||
&& localStorage.getItem('newVersionDismiss') != null
|
||||
&& parseInt(localStorage.getItem('newVersionDismiss'), 10) + (7 * 24 * 60 * 60 * 1000) > (new Date()).getTime()
|
||||
) {
|
||||
newVersionMessage.style.display = 'none';
|
||||
}
|
||||
if (newVersionDismiss != null) {
|
||||
newVersionDismiss.addEventListener('click', () => {
|
||||
localStorage.setItem('newVersionDismiss', (new Date()).getTime().toString());
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenReturnurl = document.getElementsByName('returnurl');
|
||||
if (hiddenReturnurl != null) {
|
||||
hiddenReturnurl.value = window.location.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofocus text fields
|
||||
*/
|
||||
const autofocusElements = document.querySelectorAll('.autofocus');
|
||||
let breakLoop = false;
|
||||
[].forEach.call(autofocusElements, (autofocusElement) => {
|
||||
if (autofocusElement.value === '' && !breakLoop) {
|
||||
autofocusElement.focus();
|
||||
breakLoop = true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle sub menus/forms
|
||||
*/
|
||||
const openers = document.getElementsByClassName('subheader-opener');
|
||||
if (openers != null) {
|
||||
[...openers].forEach((opener) => {
|
||||
opener.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const id = opener.getAttribute('data-open-id');
|
||||
const sub = document.getElementById(id);
|
||||
|
||||
if (sub != null) {
|
||||
[...document.getElementsByClassName('subheader-form')].forEach((element) => {
|
||||
if (element !== sub) {
|
||||
removeClass(element, 'open');
|
||||
}
|
||||
});
|
||||
|
||||
sub.classList.toggle('open');
|
||||
const autofocus = sub.querySelector('.autofocus');
|
||||
if (autofocus) {
|
||||
autofocus.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CSS target padding (for fixed bar)
|
||||
*/
|
||||
if (location.hash !== '') {
|
||||
const anchor = document.getElementById(location.hash.substr(1));
|
||||
if (anchor != null) {
|
||||
const padsize = anchor.clientHeight;
|
||||
window.scroll(0, window.scrollY - padsize);
|
||||
anchor.style.paddingTop = '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text area resizer
|
||||
*/
|
||||
const description = document.getElementById('lf_description');
|
||||
|
||||
if (description != null) {
|
||||
init(description);
|
||||
// Submit editlink form with CTRL + Enter in the text area.
|
||||
description.addEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey && event.keyCode === 13) {
|
||||
document.getElementById('button-save-edit').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarklet alert
|
||||
*/
|
||||
const bookmarkletLinks = document.querySelectorAll('.bookmarklet-link');
|
||||
const bkmMessage = document.getElementById('bookmarklet-alert');
|
||||
[].forEach.call(bookmarkletLinks, (link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
alert(bkmMessage.value);
|
||||
});
|
||||
});
|
||||
|
||||
const continent = document.getElementById('continent');
|
||||
const city = document.getElementById('city');
|
||||
if (continent != null && city != null) {
|
||||
continent.addEventListener('change', () => {
|
||||
hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
|
||||
});
|
||||
hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk actions
|
||||
*/
|
||||
const linkCheckboxes = document.querySelectorAll('.link-checkbox');
|
||||
const bar = document.getElementById('actions');
|
||||
[...linkCheckboxes].forEach((checkbox) => {
|
||||
checkbox.style.display = 'inline-block';
|
||||
checkbox.addEventListener('change', () => {
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
|
||||
const count = [...linkCheckedCheckboxes].length;
|
||||
if (count === 0 && bar.classList.contains('open')) {
|
||||
bar.classList.toggle('open');
|
||||
} else if (count > 0 && !bar.classList.contains('open')) {
|
||||
bar.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButton = document.getElementById('actions-delete');
|
||||
const token = document.getElementById('token');
|
||||
if (deleteButton != null && token != null) {
|
||||
deleteButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const links = [];
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
|
||||
[...linkCheckedCheckboxes].forEach((checkbox) => {
|
||||
links.push({
|
||||
id: checkbox.value,
|
||||
title: document.querySelector(`.linklist-item[data-id="${checkbox.value}"] .linklist-link`).innerHTML,
|
||||
});
|
||||
});
|
||||
|
||||
let message = `Are you sure you want to delete ${links.length} links?\n`;
|
||||
message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
|
||||
const ids = [];
|
||||
links.forEach((item) => {
|
||||
message += ` - ${item.title}\n`;
|
||||
ids.push(item.id);
|
||||
});
|
||||
|
||||
if (window.confirm(message)) {
|
||||
window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const changeVisibilityButtons = document.querySelectorAll('.actions-change-visibility');
|
||||
if (changeVisibilityButtons != null && token != null) {
|
||||
[...changeVisibilityButtons].forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const visibility = event.target.getAttribute('data-visibility');
|
||||
|
||||
const links = [];
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
|
||||
[...linkCheckedCheckboxes].forEach((checkbox) => {
|
||||
links.push({
|
||||
id: checkbox.value,
|
||||
title: document.querySelector(`.linklist-item[data-id="${checkbox.value}"] .linklist-link`).innerHTML,
|
||||
});
|
||||
});
|
||||
|
||||
const ids = links.map((item) => item.id);
|
||||
window.location = (
|
||||
`${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
['add', 'delete'].forEach((action) => {
|
||||
const subHeader = document.getElementById(`bulk-tag-action-${action}`);
|
||||
|
||||
if (subHeader) {
|
||||
subHeader.querySelectorAll('a.button').forEach((link) => {
|
||||
if (!link.classList.contains('action')) {
|
||||
return;
|
||||
}
|
||||
|
||||
subHeader.querySelector('input[name="tag"]').addEventListener('keypress', (event) => {
|
||||
if (event.keyCode === 13) { // enter
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const ids = [];
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
|
||||
[...linkCheckedCheckboxes].forEach((checkbox) => {
|
||||
ids.push(checkbox.value);
|
||||
});
|
||||
|
||||
subHeader.querySelector('input[name="id"]').value = ids.join(' ');
|
||||
subHeader.querySelector('form').submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Select all button
|
||||
*/
|
||||
const selectAllButtons = document.querySelectorAll('.select-all-button');
|
||||
[...selectAllButtons].forEach((selectAllButton) => {
|
||||
selectAllButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const checked = selectAllButton.classList.contains('filter-off');
|
||||
[...selectAllButtons].forEach((selectAllButton2) => {
|
||||
selectAllButton2.classList.toggle('filter-off');
|
||||
selectAllButton2.classList.toggle('filter-on');
|
||||
});
|
||||
[...linkCheckboxes].forEach((linkCheckbox) => {
|
||||
linkCheckbox.checked = checked;
|
||||
linkCheckbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tag list operations
|
||||
*
|
||||
* TODO: support error code in the backend for AJAX requests
|
||||
*/
|
||||
const tagList = document.querySelector('input[name="taglist"]');
|
||||
let existingTags = tagList ? tagList.value.split(' ') : [];
|
||||
let awesomepletes = [];
|
||||
|
||||
// Display/Hide rename form
|
||||
const renameTagButtons = document.querySelectorAll('.rename-tag');
|
||||
[...renameTagButtons].forEach((rename) => {
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const form = block.querySelector('.rename-tag-form');
|
||||
if (form.style.display === 'none' || form.style.display === '') {
|
||||
form.style.display = 'block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
}
|
||||
block.querySelector('input').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Rename a tag with an AJAX request
|
||||
const renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
|
||||
[...renameTagSubmits].forEach((rename) => {
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const input = block.querySelector('.rename-tag-input');
|
||||
const totag = input.value.replace('/"/g', '\\"');
|
||||
if (totag.trim() === '') {
|
||||
return;
|
||||
}
|
||||
const refreshedToken = document.getElementById('token').value;
|
||||
const fromtag = block.getAttribute('data-tag');
|
||||
const fromtagUrl = block.getAttribute('data-tag-url');
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${basePath}/admin/tags`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
if (xhr.status !== 200) {
|
||||
alert(`An error occurred. Return code: ${xhr.status}`);
|
||||
location.reload();
|
||||
} else {
|
||||
block.setAttribute('data-tag', totag);
|
||||
block.setAttribute('data-tag-url', encodeURIComponent(totag));
|
||||
input.setAttribute('name', totag);
|
||||
input.setAttribute('value', totag);
|
||||
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
||||
block.querySelector('a.tag-link').innerHTML = he.encode(totag);
|
||||
block
|
||||
.querySelector('a.tag-link')
|
||||
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
||||
block
|
||||
.querySelector('a.count')
|
||||
.setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
|
||||
block
|
||||
.querySelector('a.rename-tag')
|
||||
.setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
|
||||
|
||||
// Refresh awesomplete values
|
||||
existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
|
||||
}
|
||||
};
|
||||
xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
|
||||
refreshToken(basePath);
|
||||
});
|
||||
});
|
||||
|
||||
// Validate input with enter key
|
||||
const renameTagInputs = document.querySelectorAll('.rename-tag-input');
|
||||
[...renameTagInputs].forEach((rename) => {
|
||||
rename.addEventListener('keypress', (event) => {
|
||||
if (event.keyCode === 13) { // enter
|
||||
findParent(event.target, 'div', { class: 'tag-list-item' }).querySelector('.validate-rename-tag').click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete a tag with an AJAX query (alert popup confirmation)
|
||||
const deleteTagButtons = document.querySelectorAll('.delete-tag');
|
||||
[...deleteTagButtons].forEach((rename) => {
|
||||
rename.style.display = 'inline';
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const tag = block.getAttribute('data-tag');
|
||||
const tagUrl = block.getAttribute('data-tag-url');
|
||||
const refreshedToken = document.getElementById('token').value;
|
||||
|
||||
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${basePath}/admin/tags`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
block.remove();
|
||||
};
|
||||
xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
|
||||
refreshToken(basePath);
|
||||
|
||||
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
|
||||
});
|
||||
|
||||
const exportForm = document.querySelector('#exportform');
|
||||
if (exportForm != null) {
|
||||
exportForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
refreshToken(basePath, () => {
|
||||
event.target.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
|
||||
if (bulkCreationButton != null) {
|
||||
const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
|
||||
if (bulkCreationButton.classList.contains('pure-u-0')) {
|
||||
showMoreBlockElement.classList.remove('pure-u-0');
|
||||
formElement.classList.add('pure-u-0');
|
||||
} else {
|
||||
showMoreBlockElement.classList.add('pure-u-0');
|
||||
formElement.classList.remove('pure-u-0');
|
||||
}
|
||||
};
|
||||
|
||||
const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
|
||||
|
||||
toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
|
||||
bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
|
||||
});
|
||||
|
||||
// Force to send falsy value if the checkbox is not checked.
|
||||
const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
|
||||
const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
|
||||
privateButton.addEventListener('click', () => {
|
||||
privateHiddenButton.disabled = !privateHiddenButton.disabled;
|
||||
});
|
||||
privateHiddenButton.disabled = privateButton.checked;
|
||||
}
|
||||
})();
|
81
assets/myShaarli/js/plugins-admin.js
Normal file
81
assets/myShaarli/js/plugins-admin.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Change the position counter of a row.
|
||||
*
|
||||
* @param elem Element Node to change.
|
||||
* @param toPos int New position.
|
||||
*/
|
||||
function changePos(elem, toPos) {
|
||||
const elemName = elem.getAttribute('data-line');
|
||||
elem.setAttribute('data-order', toPos);
|
||||
const hiddenInput = document.querySelector(`[name="order_${elemName}"]`);
|
||||
hiddenInput.setAttribute('value', toPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row up or down.
|
||||
*
|
||||
* @param pos Element Node to move.
|
||||
* @param move int Move: +1 (down) or -1 (up)
|
||||
*/
|
||||
function changeOrder(pos, move) {
|
||||
const newpos = parseInt(pos, 10) + move;
|
||||
let lines = document.querySelectorAll(`[data-order="${pos}"]`);
|
||||
const changelines = document.querySelectorAll(`[data-order="${newpos}"]`);
|
||||
|
||||
// If we go down reverse lines to preserve the rows order
|
||||
if (move > 0) {
|
||||
lines = [].slice.call(lines).reverse();
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const parent = changelines[0].parentNode;
|
||||
changePos(lines[i], newpos);
|
||||
changePos(changelines[i], parseInt(pos, 10));
|
||||
const changeItem = move < 0 ? changelines[0] : changelines[changelines.length - 1].nextSibling;
|
||||
parent.insertBefore(lines[i], changeItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row up in the table.
|
||||
*
|
||||
* @param pos int row counter.
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
function orderUp(pos) {
|
||||
if (pos !== 0) {
|
||||
changeOrder(pos, -1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row down in the table.
|
||||
*
|
||||
* @param pos int row counter.
|
||||
*
|
||||
* @returns false
|
||||
*/
|
||||
function orderDown(pos) {
|
||||
const lastpos = parseInt(document.querySelector('[data-order]:last-child').getAttribute('data-order'), 10);
|
||||
if (pos !== lastpos) {
|
||||
changeOrder(pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Plugin admin order
|
||||
*/
|
||||
const orderPA = document.querySelectorAll('.order');
|
||||
[...orderPA].forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
if (event.target.classList.contains('order-up')) {
|
||||
orderUp(parseInt(event.target.parentNode.parentNode.getAttribute('data-order'), 10));
|
||||
} else if (event.target.classList.contains('order-down')) {
|
||||
orderDown(parseInt(event.target.parentNode.parentNode.getAttribute('data-order'), 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
1849
assets/myShaarli/scss/shaarli.scss
Normal file
1849
assets/myShaarli/scss/shaarli.scss
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,102 +0,0 @@
|
|||
## Markdown Shaarli plugin
|
||||
|
||||
Convert all your shaares description to HTML formatted Markdown.
|
||||
|
||||
[Read more about Markdown syntax](http://daringfireball.net/projects/markdown/syntax).
|
||||
|
||||
Markdown processing is done with [Parsedown library](https://github.com/erusev/parsedown).
|
||||
|
||||
### Installation
|
||||
|
||||
As a default plugin, it should already be in `tpl/plugins/` directory.
|
||||
If not, download and unpack it there.
|
||||
|
||||
The directory structure should look like:
|
||||
|
||||
```
|
||||
--- plugins
|
||||
|--- markdown
|
||||
|--- help.html
|
||||
|--- markdown.css
|
||||
|--- markdown.meta
|
||||
|--- markdown.php
|
||||
|--- README.md
|
||||
```
|
||||
|
||||
To enable the plugin, just check it in the plugin administration page.
|
||||
|
||||
You can also add `markdown` to your list of enabled plugins in `data/config.json.php`
|
||||
(`general.enabled_plugins` list).
|
||||
|
||||
This should look like:
|
||||
|
||||
```
|
||||
"general": {
|
||||
"enabled_plugins": [
|
||||
"markdown",
|
||||
[...]
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`,
|
||||
or the `master` branch, run
|
||||
|
||||
composer update --no-dev --prefer-dist
|
||||
|
||||
### No Markdown tag
|
||||
|
||||
If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax.
|
||||
|
||||
> Note: this is a special tag, so it won't be displayed in link list.
|
||||
|
||||
### HTML escape
|
||||
|
||||
By default, HTML tags are escaped. You can enable HTML tags rendering
|
||||
by setting `security.markdwon_escape` to `false` in `data/config.json.php`:
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"markdown_escape": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With this setting, Markdown support HTML tags. For example:
|
||||
|
||||
> <strong>strong</strong><strike>strike</strike>
|
||||
|
||||
Will render as:
|
||||
|
||||
> <strong>strong</strong><strike>strike</strike>
|
||||
|
||||
|
||||
**Warning:**
|
||||
|
||||
* This setting might present **security risks** (XSS) on shared instances, even though tags
|
||||
such as script, iframe, etc should be disabled.
|
||||
* If you want to shaare HTML code, it is necessary to use inline code or code blocks.
|
||||
* If your shaared descriptions contained HTML tags before enabling the markdown plugin,
|
||||
enabling it might break your page.
|
||||
|
||||
### Known issue
|
||||
|
||||
#### Redirector
|
||||
|
||||
If you're using a redirector, you *need* to add a space after a link,
|
||||
otherwise the rest of the line will be `urlencode`.
|
||||
|
||||
```
|
||||
[link](http://domain.tld)-->test
|
||||
```
|
||||
|
||||
Will consider `http://domain.tld)-->test` as URL.
|
||||
|
||||
Instead, add an additional space.
|
||||
|
||||
```
|
||||
[link](http://domain.tld) -->test
|
||||
```
|
||||
|
||||
> Won't fix because a `)` is a valid part of an URL.
|
|
@ -1,5 +0,0 @@
|
|||
<div class="md_help">
|
||||
%s
|
||||
<a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
|
||||
%s</a>.
|
||||
</div>
|
|
@ -1,173 +0,0 @@
|
|||
/**
|
||||
* Credit to Simon Laroche <https://github.com/simonlc/Markdown-CSS>
|
||||
* whom created the CSS which this file is based on.
|
||||
* License: Unlicense <http://unlicense.org/>
|
||||
*/
|
||||
|
||||
.markdown p{
|
||||
margin:0.75em 0;
|
||||
}
|
||||
|
||||
.markdown img{
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
.markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6{
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
line-height:1em;
|
||||
margin:0.75em 0;
|
||||
}
|
||||
.markdown h4, .markdown h5, .markdown h6{ font-weight: bold; }
|
||||
.markdown h1{ font-size:2.5em; }
|
||||
.markdown h2{ font-size:2em; }
|
||||
.markdown h3{ font-size:1.5em; }
|
||||
.markdown h4{ font-size:1.2em; }
|
||||
.markdown h5{ font-size:1em; }
|
||||
.markdown h6{ font-size:0.9em; }
|
||||
|
||||
.markdown blockquote{
|
||||
color:#666666;
|
||||
padding-left: 3em;
|
||||
border-left: 0.5em #EEE solid;
|
||||
margin:0.75em 0;
|
||||
}
|
||||
.markdown hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; }
|
||||
.markdown pre, .markdown code, .markdown kbd, .markdown samp {
|
||||
font-family: monospace, 'courier new';
|
||||
font-size: 0.98em;
|
||||
}
|
||||
.markdown pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
|
||||
|
||||
.markdown b, .markdown strong { font-weight: bold; }
|
||||
|
||||
.markdown dfn, .markdown em { font-style: italic; }
|
||||
|
||||
.markdown ins { background: #ff9; color: #000; text-decoration: none; }
|
||||
|
||||
.markdown mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
|
||||
|
||||
.markdown sub, .markdown sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
||||
.markdown sup { top: -0.5em; }
|
||||
.markdown sub { bottom: -0.25em; }
|
||||
|
||||
.markdown ul, .markdown ol { margin: 1em 0; padding: 0 0 0 2em; }
|
||||
.markdown li p:last-child { margin:0 }
|
||||
.markdown dd { margin: 0 0 0 2em; }
|
||||
|
||||
.markdown img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
|
||||
|
||||
.markdown table { border-collapse: collapse; border-spacing: 0; }
|
||||
.markdown td { vertical-align: top; }
|
||||
|
||||
@media only screen and (min-width: 480px) {
|
||||
.markdown {font-size:0.9em;}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.markdown {font-size:1em;}
|
||||
}
|
||||
|
||||
#linklist .markdown li {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
#linklist .markdown ul li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
#linklist .markdown ol li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown table {
|
||||
padding: 0;
|
||||
}
|
||||
.markdown table tr {
|
||||
border-top: 1px solid #cccccc;
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown table tr:nth-child(2n) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.markdown table tr th {
|
||||
font-weight: bold;
|
||||
border: 1px solid #cccccc;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
.markdown table tr td {
|
||||
border: 1px solid #cccccc;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 6px 13px;
|
||||
}
|
||||
.markdown table tr th :first-child, .markdown table tr td :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown table tr th :last-child, table tr td :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown pre {
|
||||
background-color: #eee;
|
||||
padding: 4px 9px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
box-shadow: 0 -1px 0 #e5e5e5,0 0 1px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24);
|
||||
}
|
||||
|
||||
.markdown pre code {
|
||||
color: black;
|
||||
font-family: 'Consolas', 'Monaco', 'Andale Mono', monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.7;
|
||||
font-size: 11.5px;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
.markdown :not(pre) code {
|
||||
background-color: #eee;
|
||||
padding: 1px 3px;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 -1px 0 #e5e5e5,0 0 1px rgba(0,0,0,0.12),0 1px 1px rgba(0,0,0,0.24);
|
||||
}
|
||||
|
||||
#pageheader .md_help {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove header links style
|
||||
*/
|
||||
#pageheader .md_help a {
|
||||
color: lightgray;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#pageheader .md_help a:hover {
|
||||
color: white;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
|
||||
If your shaared descriptions contained HTML tags before enabling the markdown plugin,
|
||||
enabling it might break your page.
|
||||
See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering\">README</a>."
|
|
@ -1,365 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Markdown.
|
||||
*
|
||||
* Shaare's descriptions are parsed with Markdown.
|
||||
*/
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
|
||||
/*
|
||||
* If this tag is used on a shaare, the description won't be processed by Parsedown.
|
||||
*/
|
||||
define('NO_MD_TAG', 'nomarkdown');
|
||||
|
||||
/**
|
||||
* Parse linklist descriptions.
|
||||
*
|
||||
* @param array $data linklist data.
|
||||
* @param ConfigManager $conf instance.
|
||||
*
|
||||
* @return mixed linklist data parsed in markdown (and converted to HTML).
|
||||
*/
|
||||
function hook_markdown_render_linklist($data, $conf)
|
||||
{
|
||||
foreach ($data['links'] as &$value) {
|
||||
if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
|
||||
$value = stripNoMarkdownTag($value);
|
||||
continue;
|
||||
}
|
||||
$value['description_src'] = $value['description'];
|
||||
$value['description'] = process_markdown(
|
||||
$value['description'],
|
||||
$conf->get('security.markdown_escape', true),
|
||||
$conf->get('security.allowed_protocols')
|
||||
);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse feed linklist descriptions.
|
||||
*
|
||||
* @param array $data linklist data.
|
||||
* @param ConfigManager $conf instance.
|
||||
*
|
||||
* @return mixed linklist data parsed in markdown (and converted to HTML).
|
||||
*/
|
||||
function hook_markdown_render_feed($data, $conf)
|
||||
{
|
||||
foreach ($data['links'] as &$value) {
|
||||
if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
|
||||
$value = stripNoMarkdownTag($value);
|
||||
continue;
|
||||
}
|
||||
$value['description'] = reverse_feed_permalink($value['description']);
|
||||
$value['description'] = process_markdown(
|
||||
$value['description'],
|
||||
$conf->get('security.markdown_escape', true),
|
||||
$conf->get('security.allowed_protocols')
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse daily descriptions.
|
||||
*
|
||||
* @param array $data daily data.
|
||||
* @param ConfigManager $conf instance.
|
||||
*
|
||||
* @return mixed daily data parsed in markdown (and converted to HTML).
|
||||
*/
|
||||
function hook_markdown_render_daily($data, $conf)
|
||||
{
|
||||
//var_dump($data);die;
|
||||
// Manipulate columns data
|
||||
foreach ($data['linksToDisplay'] as &$value) {
|
||||
if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
|
||||
$value = stripNoMarkdownTag($value);
|
||||
continue;
|
||||
}
|
||||
$value['formatedDescription'] = process_markdown(
|
||||
$value['formatedDescription'],
|
||||
$conf->get('security.markdown_escape', true),
|
||||
$conf->get('security.allowed_protocols')
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if noMarkdown is set in tags.
|
||||
*
|
||||
* @param string $tags tag list
|
||||
*
|
||||
* @return bool true if markdown should be disabled on this link.
|
||||
*/
|
||||
function noMarkdownTag($tags)
|
||||
{
|
||||
return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the no-markdown meta tag so it won't be displayed.
|
||||
*
|
||||
* @param array $link Link data.
|
||||
*
|
||||
* @return array Updated link without no markdown tag.
|
||||
*/
|
||||
function stripNoMarkdownTag($link)
|
||||
{
|
||||
if (! empty($link['taglist'])) {
|
||||
$offset = array_search(NO_MD_TAG, $link['taglist']);
|
||||
if ($offset !== false) {
|
||||
unset($link['taglist'][$offset]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($link['tags'])) {
|
||||
str_replace(NO_MD_TAG, '', $link['tags']);
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* When link list is displayed, include markdown CSS.
|
||||
*
|
||||
* @param array $data includes data.
|
||||
*
|
||||
* @return mixed - includes data with markdown CSS file added.
|
||||
*/
|
||||
function hook_markdown_render_includes($data)
|
||||
{
|
||||
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
|
||||
|| $data['_PAGE_'] == Router::$PAGE_DAILY
|
||||
|| $data['_PAGE_'] == Router::$PAGE_EDITLINK
|
||||
) {
|
||||
$data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook render_editlink.
|
||||
* Adds an help link to markdown syntax.
|
||||
*
|
||||
* @param array $data data passed to plugin
|
||||
*
|
||||
* @return array altered $data.
|
||||
*/
|
||||
function hook_markdown_render_editlink($data)
|
||||
{
|
||||
// Load help HTML into a string
|
||||
$txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
|
||||
$translations = [
|
||||
t('Description will be rendered with'),
|
||||
t('Markdown syntax documentation'),
|
||||
t('Markdown syntax'),
|
||||
];
|
||||
$data['edit_link_plugin'][] = vsprintf($txt, $translations);
|
||||
// Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
|
||||
if (! in_array(NO_MD_TAG, $data['tags'])) {
|
||||
$data['tags'][NO_MD_TAG] = 0;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove HTML links auto generated by Shaarli core system.
|
||||
* Keeps HREF attributes.
|
||||
*
|
||||
* @param string $description input description text.
|
||||
*
|
||||
* @return string $description without HTML links.
|
||||
*/
|
||||
function reverse_text2clickable($description)
|
||||
{
|
||||
$descriptionLines = explode(PHP_EOL, $description);
|
||||
$descriptionOut = '';
|
||||
$codeBlockOn = false;
|
||||
$lineCount = 0;
|
||||
|
||||
foreach ($descriptionLines as $descriptionLine) {
|
||||
// Detect line of code: starting with 4 spaces,
|
||||
// except lists which can start with +/*/- or `2.` after spaces.
|
||||
$codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
|
||||
// Detect and toggle block of code
|
||||
if (!$codeBlockOn) {
|
||||
$codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
|
||||
} elseif (preg_match('/^```/', $descriptionLine) > 0) {
|
||||
$codeBlockOn = false;
|
||||
}
|
||||
|
||||
$hashtagTitle = ' title="Hashtag [^"]+"';
|
||||
// Reverse `inline code` hashtags.
|
||||
$descriptionLine = preg_replace(
|
||||
'!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m',
|
||||
'$1$2$3',
|
||||
$descriptionLine
|
||||
);
|
||||
|
||||
// Reverse all links in code blocks, only non hashtag elsewhere.
|
||||
$hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?';
|
||||
$descriptionLine = preg_replace(
|
||||
'#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m',
|
||||
'$1',
|
||||
$descriptionLine
|
||||
);
|
||||
|
||||
// Make hashtag links markdown ready, otherwise the links will be ignored with escape set to true
|
||||
if (!$codeBlockOn && !$codeLineOn) {
|
||||
$descriptionLine = preg_replace(
|
||||
'#<a href="([^ ]*)"'. $hashtagTitle .'>([^<]+)</a>#m',
|
||||
'[$2]($1)',
|
||||
$descriptionLine
|
||||
);
|
||||
}
|
||||
|
||||
$descriptionOut .= $descriptionLine;
|
||||
if ($lineCount++ < count($descriptionLines) - 1) {
|
||||
$descriptionOut .= PHP_EOL;
|
||||
}
|
||||
}
|
||||
return $descriptionOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <br> tag to let markdown handle it.
|
||||
*
|
||||
* @param string $description input description text.
|
||||
*
|
||||
* @return string $description without <br> tags.
|
||||
*/
|
||||
function reverse_nl2br($description)
|
||||
{
|
||||
return preg_replace('!<br */?>!im', '', $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove HTML spaces ' ' auto generated by Shaarli core system.
|
||||
*
|
||||
* @param string $description input description text.
|
||||
*
|
||||
* @return string $description without HTML links.
|
||||
*/
|
||||
function reverse_space2nbsp($description)
|
||||
{
|
||||
return preg_replace('/(^| ) /m', '$1 ', $description);
|
||||
}
|
||||
|
||||
function reverse_feed_permalink($description)
|
||||
{
|
||||
return preg_replace('@— <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '— [$2]($1)', $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace not whitelisted protocols with http:// in given description.
|
||||
*
|
||||
* @param string $description input description text.
|
||||
* @param array $allowedProtocols list of allowed protocols.
|
||||
*
|
||||
* @return string $description without malicious link.
|
||||
*/
|
||||
function filter_protocols($description, $allowedProtocols)
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'#]\((.*?)\)#is',
|
||||
function ($match) use ($allowedProtocols) {
|
||||
return ']('. whitelist_protocols($match[1], $allowedProtocols) .')';
|
||||
},
|
||||
$description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dangerous HTML tags (tags, iframe, etc.).
|
||||
* Doesn't affect <code> content (already escaped by Parsedown).
|
||||
*
|
||||
* @param string $description input description text.
|
||||
*
|
||||
* @return string given string escaped.
|
||||
*/
|
||||
function sanitize_html($description)
|
||||
{
|
||||
$escapeTags = array(
|
||||
'script',
|
||||
'style',
|
||||
'link',
|
||||
'iframe',
|
||||
'frameset',
|
||||
'frame',
|
||||
);
|
||||
foreach ($escapeTags as $tag) {
|
||||
$description = preg_replace_callback(
|
||||
'#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
|
||||
function ($match) {
|
||||
return escape($match[0]);
|
||||
},
|
||||
$description
|
||||
);
|
||||
}
|
||||
$description = preg_replace(
|
||||
'#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
|
||||
'$1',
|
||||
$description
|
||||
);
|
||||
return $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render shaare contents through Markdown parser.
|
||||
* 1. Remove HTML generated by Shaarli core.
|
||||
* 2. Reverse the escape function.
|
||||
* 3. Generate markdown descriptions.
|
||||
* 4. Sanitize sensible HTML tags for security.
|
||||