Shaarli_bm_theme/shaarli-pro/js/shaarit-rules.js

378 lines
14 KiB
JavaScript

/**
* shaarit-rules.js
*
* Règles métier partagées avec l'application Android ShaarIt.
* Source de vérité unique pour :
* - la détection des notes / todos / épinglés ;
* - les tags techniques cachés par défaut ;
* - la détection de type de contenu (video, podcast, radio, music, article,
* news, social, repository/dev, shopping, image, pdf, document) ;
* - la génération d'URLs internes (notes / todos).
*
* Non destructif : toutes les conventions existantes du thème web continuent
* d'être reconnues en lecture. Les règles Android viennent s'y ajouter.
*
* Exposé sur `window.ShaarItRules`.
*/
(function (global) {
"use strict";
// ---------------------------------------------------------------------------
// Tags système (cachés par défaut côté UI)
// ---------------------------------------------------------------------------
var PRESET_SYSTEM_TAGS = [
{ name: "note", desc: "Notes - Identifiant interne", hidden: true },
{ name: "shaarli-note", desc: "Notes - Alias legacy", hidden: true },
{ name: "todo", desc: "Tâches - Identifiant interne", hidden: true },
{ name: "shaarli-todo", desc: "Tâches - Alias legacy", hidden: true },
{ name: "shaarli-pin", desc: "Épinglé - Favoris en haut", hidden: true },
{ name: "note-color-*", desc: "Couleurs des notes (wildcard)", hidden: true },
{ name: "notebg-*", desc: "Fonds des notes (wildcard)", hidden: true },
{ name: "notefilter-*", desc: "Filtres des notes (wildcard)", hidden: true },
{ name: "font-*", desc: "Couleur de police (wildcard)", hidden: true },
{ name: "readitlater", desc: "À lire plus tard", hidden: true },
{ name: "brain-dump", desc: "Capture rapide d'idées", hidden: true },
{ name: "shaarli-archive", desc: "Archivé", hidden: false },
{ name: "shaarli-archiver",desc: "Archivé - Alias legacy", hidden: false }
];
// ---------------------------------------------------------------------------
// Détection d'entités (note / todo / pin) - compatibilité Android + legacy
// ---------------------------------------------------------------------------
function toLower(x) { return String(x || "").toLowerCase(); }
function asTagArray(tags) {
if (Array.isArray(tags)) return tags.map(toLower);
if (typeof tags === "string") {
return tags.split(/[\s,|]+/).map(toLower).filter(Boolean);
}
return [];
}
/**
* Détection d'une note.
* Compatible avec les règles Android :
* - URL `note://` (mais pas `note://todo-`)
* - URL `http://shaare` / `/shaare`
* - URL `https://shaarit.app/note/...`
* - Tag `note`, `#note`, `shaarli-note`
*/
function isNote(link) {
if (!link) return false;
var url = String(link.url || "").trim();
var tags = asTagArray(link.tags);
if (url) {
var u = url.toLowerCase();
if (u.indexOf("note://") === 0 && u.indexOf("note://todo-") !== 0) return true;
if (u.indexOf("http://shaare") === 0) return true;
if (u.indexOf("/shaare") === 0) return true;
if (u.indexOf("https://shaarit.app/note/") === 0) return true;
if (u.indexOf("http://shaarit.app/note/") === 0) return true;
}
for (var i = 0; i < tags.length; i++) {
if (tags[i] === "note" || tags[i] === "#note" || tags[i] === "shaarli-note") return true;
}
return false;
}
/**
* Détection d'une tâche.
* Compatible avec les règles Android :
* - URL `note://todo-...`
* - URL `https://shaarit.app/todo/...`
* - URL `http://shaarli-todo` (legacy web)
* - Tag `todo`, `#todo`, `shaarli-todo`
*/
function isTodo(link) {
if (!link) return false;
var url = String(link.url || "").trim();
var tags = asTagArray(link.tags);
if (url) {
var u = url.toLowerCase();
if (u.indexOf("note://todo-") === 0) return true;
if (u.indexOf("https://shaarit.app/todo/") === 0) return true;
if (u.indexOf("http://shaarit.app/todo/") === 0) return true;
if (u.indexOf("http://shaarli-todo") === 0) return true;
if (u.indexOf("https://shaarli-todo") === 0) return true;
}
for (var i = 0; i < tags.length; i++) {
if (tags[i] === "todo" || tags[i] === "#todo" || tags[i] === "shaarli-todo") return true;
}
return false;
}
/** Détection épinglé via tag `shaarli-pin`. */
function isPinned(link) {
if (!link) return false;
var tags = asTagArray(link.tags);
return tags.indexOf("shaarli-pin") !== -1;
}
/** Détection archive (tag `shaarli-archive` ou legacy `shaarli-archiver`). */
function isArchived(link) {
if (!link) return false;
var tags = asTagArray(link.tags);
return tags.indexOf("shaarli-archive") !== -1 || tags.indexOf("shaarli-archiver") !== -1;
}
// ---------------------------------------------------------------------------
// Génération d'URLs internes (style Android)
// ---------------------------------------------------------------------------
function randomId() {
// Identifiant court type base36, 12 caractères.
if (global.crypto && global.crypto.getRandomValues) {
var arr = new Uint8Array(9);
global.crypto.getRandomValues(arr);
var out = "";
for (var i = 0; i < arr.length; i++) {
out += ("0" + arr[i].toString(36)).slice(-2);
}
return out.substring(0, 12);
}
return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).substring(0, 12);
}
function generateNoteUrl() {
return "https://shaarit.app/note/" + randomId();
}
function generateTodoUrl() {
return "https://shaarit.app/todo/" + randomId();
}
// ---------------------------------------------------------------------------
// Détection de type de contenu (ContentType, règles Android)
// ---------------------------------------------------------------------------
var CONTENT_TYPES = {
UNKNOWN: "unknown",
ARTICLE: "article",
VIDEO: "video",
PODCAST: "podcast",
IMAGE: "image",
PDF: "pdf",
REPOSITORY: "repository",
DOCUMENT: "document",
SOCIAL: "social",
SHOPPING: "shopping",
NEWSLETTER: "newsletter",
MUSIC: "music",
RADIO: "radio",
NEWS: "news"
};
// Tags auto-ajoutés par type de contenu (conformes règles Android UI).
var CONTENT_TYPE_TAGS = {
video: ["video"],
podcast: ["podcast"],
radio: ["radio"],
music: ["music"],
article: ["article"],
news: ["news"],
social: ["social"],
repository: ["repository", "dev"],
shopping: ["shopping"],
newsletter: ["newsletter"],
image: ["image"],
pdf: ["pdf"],
document: [],
unknown: []
};
function parseHost(url) {
try {
var u = new URL(url);
return (u.hostname || "").toLowerCase();
} catch (e) {
// Fallback très simple si URL invalide
var m = String(url || "").match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i);
return m ? m[1].toLowerCase() : "";
}
}
function hostContainsAny(host, list) {
for (var i = 0; i < list.length; i++) {
if (host.indexOf(list[i]) !== -1) return true;
}
return false;
}
// --- Audio : RADIO > PODCAST > MUSIC ---
var RADIO_HOSTS = [
"playerservices.streamtheworld.com", "icecast", "shoutcast",
"fluxradios.com", "tunein.com", "radio.garden", "mytuner-radio.com",
"iheart.com", "onlineradiobox.com", "radio.net"
];
function isRadio(url, host) {
if (/\.(m3u|m3u8|pls)(\?|$)/i.test(url)) return true;
if (hostContainsAny(host, RADIO_HOSTS)) return true;
if (host.indexOf("stream.") === 0 || host.indexOf("live.") === 0) return true;
if (host.indexOf("ici.radio-canada.ca") !== -1 || host.indexOf("radio-canada.ca") !== -1) {
if (/\/balados(\/|$)/i.test(url)) return false;
if (/\/(direct|premiere|audio-fil)(\/|$)/i.test(url)) return true;
}
return false;
}
var PODCAST_HOSTS = [
"podcasts.apple.com", "overcast.fm", "pocketcasts.com", "castbox.fm",
"stitcher.com", "acast.com", "anchor.fm", "libsyn.com",
"simplecast.com", "buzzsprout.com"
];
function isPodcast(url, host) {
if (hostContainsAny(host, PODCAST_HOSTS)) return true;
if (/open\.spotify\.com\/(show|episode)\//i.test(url)) return true;
if (/\/balados(\/|$)|\/ohdio\/balados(\/|$)/i.test(url)) return true;
if (/\.(xml|rss)(\?|$)/i.test(url)) return true;
return false;
}
var MUSIC_HOSTS = [
"music.apple.com", "deezer.com", "tidal.com", "music.youtube.com",
"bandcamp.com", "soundcloud.com", "mixcloud.com", "beatport.com"
];
function isMusic(url, host) {
if (hostContainsAny(host, MUSIC_HOSTS)) return true;
if (/open\.spotify\.com\/(track|album|artist|playlist)\//i.test(url)) return true;
return false;
}
// --- Catégories web ---
var VIDEO_HOSTS = ["youtube.com", "youtu.be", "vimeo.com", "dailymotion.com", "twitch.tv", "netflix.com"];
function isVideo(host) { return hostContainsAny(host, VIDEO_HOSTS); }
var SOCIAL_HOSTS = [
"facebook.com", "instagram.com", "tiktok.com", "twitter.com", "x.com",
"linkedin.com", "reddit.com", "snapchat.com", "pinterest.com", "mastodon"
];
function isSocial(host) { return hostContainsAny(host, SOCIAL_HOSTS); }
var REPO_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "stackoverflow.com"];
function isRepository(host) { return hostContainsAny(host, REPO_HOSTS); }
var SHOPPING_HOSTS = ["amazon", "ebay", "etsy.com", "aliexpress.com", "shopify"];
function isShopping(host) { return hostContainsAny(host, SHOPPING_HOSTS); }
var DOCUMENT_HOSTS = [
"docs.google.com", "drive.google.com", "notion.so", "trello.com",
"jira", "confluence"
];
function isDocument(host) { return hostContainsAny(host, DOCUMENT_HOSTS); }
var NEWS_HOSTS = [
"news", "nytimes", "lemonde", "bbc", "cnn", "reuters",
"theguardian", "lefigaro"
];
function isNews(host) { return hostContainsAny(host, NEWS_HOSTS); }
var NEWSLETTER_HOSTS = ["substack", "revue", "mailchimp"];
function isNewsletter(host) { return hostContainsAny(host, NEWSLETTER_HOSTS); }
var IMAGE_HOSTS = ["imgur.com", "flickr.com"];
function isImageUrl(url, host) {
if (/\.(jpe?g|png|gif|webp|bmp|svg)(\?|$)/i.test(url)) return true;
return hostContainsAny(host, IMAGE_HOSTS);
}
function isPdfUrl(url) {
return /\.pdf(\?|$)/i.test(url);
}
/**
* Détecte le type de contenu d'une URL (règles Android).
* @returns {{type: string, tags: string[]}}
*/
function detectContentType(url) {
var raw = String(url || "").trim();
if (!raw) return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
// URLs internes : pas de détection
var low = raw.toLowerCase();
if (low.indexOf("note://") === 0 ||
low.indexOf("https://shaarit.app/note/") === 0 ||
low.indexOf("https://shaarit.app/todo/") === 0 ||
low.indexOf("http://shaare") === 0 ||
low.indexOf("/shaare") === 0 ||
low.indexOf("http://shaarli-todo") === 0) {
return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
}
var host = parseHost(raw);
// 1. Fichiers (priorité haute sur l'extension)
if (isPdfUrl(raw)) return { type: CONTENT_TYPES.PDF, tags: CONTENT_TYPE_TAGS.pdf };
if (isImageUrl(raw, host))return { type: CONTENT_TYPES.IMAGE, tags: CONTENT_TYPE_TAGS.image };
// 2. Audio : RADIO > PODCAST > MUSIC
if (isRadio(raw, host)) return { type: CONTENT_TYPES.RADIO, tags: CONTENT_TYPE_TAGS.radio };
if (isPodcast(raw, host)) return { type: CONTENT_TYPES.PODCAST, tags: CONTENT_TYPE_TAGS.podcast };
if (isMusic(raw, host)) return { type: CONTENT_TYPES.MUSIC, tags: CONTENT_TYPE_TAGS.music };
// 3. Vidéo
if (isVideo(host)) return { type: CONTENT_TYPES.VIDEO, tags: CONTENT_TYPE_TAGS.video };
// 4. Plateformes spécifiques
if (isRepository(host)) return { type: CONTENT_TYPES.REPOSITORY, tags: CONTENT_TYPE_TAGS.repository };
if (isDocument(host)) return { type: CONTENT_TYPES.DOCUMENT, tags: CONTENT_TYPE_TAGS.document };
if (isSocial(host)) return { type: CONTENT_TYPES.SOCIAL, tags: CONTENT_TYPE_TAGS.social };
if (isShopping(host)) return { type: CONTENT_TYPES.SHOPPING, tags: CONTENT_TYPE_TAGS.shopping };
if (isNewsletter(host)) return { type: CONTENT_TYPES.NEWSLETTER, tags: CONTENT_TYPE_TAGS.newsletter };
if (isNews(host)) return { type: CONTENT_TYPES.NEWS, tags: CONTENT_TYPE_TAGS.news };
return { type: CONTENT_TYPES.UNKNOWN, tags: [] };
}
/**
* Fusion d'un tableau de tags existants avec les tags auto-détectés.
* Conserve l'ordre existant, ajoute uniquement les tags manquants.
*/
function mergeAutoTags(existingTags, autoTags) {
var list = Array.isArray(existingTags) ? existingTags.slice() : [];
var lower = list.map(toLower);
(autoTags || []).forEach(function (t) {
var clean = String(t || "").trim();
if (!clean) return;
if (lower.indexOf(clean.toLowerCase()) === -1) {
list.push(clean);
lower.push(clean.toLowerCase());
}
});
return list;
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
var api = {
PRESET_SYSTEM_TAGS: PRESET_SYSTEM_TAGS,
CONTENT_TYPES: CONTENT_TYPES,
CONTENT_TYPE_TAGS: CONTENT_TYPE_TAGS,
isNote: isNote,
isTodo: isTodo,
isPinned: isPinned,
isArchived: isArchived,
generateNoteUrl: generateNoteUrl,
generateTodoUrl: generateTodoUrl,
detectContentType: detectContentType,
mergeAutoTags: mergeAutoTags
};
global.ShaarItRules = api;
})(typeof window !== "undefined" ? window : this);