Skip to content

Commit

Permalink
Improve image identification and category sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien-schiehle authored Jul 16, 2022
1 parent e9fe7ed commit 7b8529e
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 98 deletions.
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"WA.Sync": "WA Sync",
"WA.OnWA": "On World Anvil",

"WA.ButtonRefreshAll": "Refresh All",
"WA.ButtonImportAll": "Import All",
"WA.ButtonSyncAll": "Sync All",
"WA.ButtonVisibilityHide": "Hide from players",
Expand Down
2 changes: 1 addition & 1 deletion module/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class WorldAnvilConfig extends FormApplication {
game.settings.register("world-anvil", "configuration", {
scope: "world",
config: false,
default: null,
default: {},
type: Object,
onChange: async c => {
const anvil = game.modules.get("world-anvil").anvil;
Expand Down
190 changes: 94 additions & 96 deletions module/framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export function getArticleContent(article) {
});

// Filter sections, removing ignored ones.
const ignoredSectionIds = [DISPLAY_SIDEBAR_SECTION_ID, "issystem"];
const ignoredSectionIds = [DISPLAY_SIDEBAR_SECTION_ID, "issystem", "folderId"];
const filteredEntries = sectionEntries.filter( ([id, section]) => {
if( ignoredSectionIds.includes(id) ) { return false; }
if( !includeSidebars ) {
Expand Down Expand Up @@ -294,41 +294,67 @@ export function getArticleContent(article) {
content += aside;
content += sections;

const htmlContent = parsedContentToHTML(content);
const image = chooseJournalEntyImage(article, htmlContent);

// Return content, image and flags
const parsedData = {
html: htmlContent.innerHTML,
img: image,
waFlags: waFlags
}
/**
* A hook event that fires when a WorldAnvil article is parsed
* @function WACreateJournalEntry
* @memberof hookEvents
* @param {Article} article The original Article
* @param {ParsedArticleResult} parsedData The parsed article content
*/
Hooks.callAll(`WAParseArticle`, article, parsedData);
return parsedData;
}

/**
* Modify content by substituting image paths, adding paragraph break and wa-link elements
* @param {string} content parsed article content
* @returns {HTMLElement} formated content, inside a HTML div element
*/
export function parsedContentToHTML(content) {

// Disable image source attributes so that they do not begin loading immediately
content = content.replace(/src=/g, "data-src=");

// HTML formatting
const div = document.createElement("div");
div.innerHTML = content;
const htmlElement = document.createElement("div");
htmlElement.innerHTML = content;

// Paragraph Breaks
const t = document.createTextNode("%p%");
div.querySelectorAll("span.line-spacer").forEach(s => s.parentElement.replaceChild(t.cloneNode(), s));

// Portrait Image as Featured or Cover image if no Portrait
let image = null;
if ( article.portrait ) {
image = article.portrait.url.replace("http://", "https://");
} else if ( article.cover ) {
image = article.cover.url.replace("http://", "https://");
}
htmlElement.querySelectorAll("span.line-spacer").forEach(s => s.parentElement.replaceChild(t.cloneNode(), s));

// Image from body
div.querySelectorAll("img").forEach(i => {
htmlElement.querySelectorAll("img").forEach(i => {

// Default href link to hosted foundry server, and not WA. => it needs to be set
if( i.parentElement.tagName === "A" ) {
i.parentElement.href = `https://worldanvil.com/${i.parentElement.pathname}`;
}

// Set image source
let img = new Image();
img.src = `https://worldanvil.com${i.dataset.src}`;
delete i.dataset.src;
img.alt = i.alt;
img.title = i.title;
img.style.cssText = i.style.cssText; // Retain custum sizing
i.parentElement.replaceChild(img, i);
image = image || img.src;
});

// World Anvil Content Links
div.querySelectorAll('span[data-article-id]').forEach(el => {
htmlElement.querySelectorAll('span[data-article-id]').forEach(el => {
el.classList.add("entity-link", "wa-link");
});
div.querySelectorAll('a[data-article-id]').forEach(el => {
htmlElement.querySelectorAll('a[data-article-id]').forEach(el => {
el.classList.add("entity-link", "wa-link");
const span = document.createElement("span");
span.classList = el.classList;
Expand All @@ -338,24 +364,31 @@ export function getArticleContent(article) {
});

// Regex formatting
let html = div.innerHTML;
html = html.replace(/%p%/g, "</p>\n<p>");
htmlElement.innerHTML = htmlElement.innerHTML.replace(/%p%/g, "</p>\n<p>");
return htmlElement;
}

// Return content, image and flags
const parsedData = {
html: html,
img: image,
waFlags: waFlags
/**
* Retrive the image that will be displayed as the journal entry image
* @param {Article} article Wa article
* @param {HTMLElement} htmlContent Journal entry content, in html format
* @returns {string|null} The featured image path, or null if no image was present
*/
function chooseJournalEntyImage( article, htmlContent ) {

// Case 1 : There is a portrait Image
if ( article.portrait ) {
return article.portrait.url.replace("http://", "https://");
}

// Case 2 : There is a cover Image
if ( article.cover ) {
return article.cover.url.replace("http://", "https://");
}
/**
* A hook event that fires when a WorldAnvil article is parsed
* @function WACreateJournalEntry
* @memberof hookEvents
* @param {Article} article The original Article
* @param {ParsedArticleResult} parsedData The parsed article content
*/
Hooks.callAll(`WAParseArticle`, article, parsedData);
return parsedData;

// Default behavior : Take the first image inside article content
const images = htmlContent.querySelectorAll("img");
return images[0]?.src || null;
}

/* -------------------------------------------- */
Expand Down Expand Up @@ -387,24 +420,12 @@ export async function getCategories({cache=true}={}) {
// Get the category mapping
const categories = await _getCategories({cache});

// Build the tree structure
let _depth = 0;
const tree = categories.get(CATEGORY_ID.root);
const pending = Array.from(categories.values()).filter(c => c.id !== CATEGORY_ID.root);
const unmapped = _buildCategoryBranch(tree, pending, _depth);

// Add un-mapped categories as children of the root
if ( unmapped.length ) {
unmapped.sort(_sortCategories);
for ( let c of unmapped ) {
console.warn(`World-Anvil | Category ${c.title} failed to map to a parent category`);
c.parent = undefined;
tree.children.push(c);
}
}

// Associate categories with Folder documents
associateCategoryFolders(categories);

// Tree starts with root
const tree = categories.get(CATEGORY_ID.root);

return {categories, tree};
}

Expand Down Expand Up @@ -439,6 +460,7 @@ async function _getCategories({cache=true}={}) {
id: CATEGORY_ID.root,
title: `[WA] ${anvil.world.name}`,
position: 0,
copyForSort: [],
children: [],
folder: null
};
Expand All @@ -449,19 +471,41 @@ async function _getCategories({cache=true}={}) {
id: CATEGORY_ID.uncategorized,
title: game.i18n.localize('WA.CategoryUncategorized'),
position: 9e9,
copyForSort: [],
children : [],
parent: root,
isUncategorized: true
};
categories.set(uncategorized.id, uncategorized);

// Retrieve categories from the World Anvil API
// Retrieve categories from the World Anvil API (build map)
const request = await anvil.getCategories();
for ( let c of (request?.categories || []) ) {
categories.set(c.id, c);
c.copyForSort = c.children?.categories ?? [];
c.children = [];
c.folder = undefined;
categories.set(c.id, c);
}
// Append children
for( let c of (request?.categories || []) ) {
const parentId = c.parentCategory?.id ?? CATEGORY_ID.root;
const parent = categories.get(parentId);
c.parent = parent;
parent.children.push(c);
}
// Sort children
for( let c of categories.values() ) {

c.children.sort( (a,b) => {
const indexA = c.copyForSort.findIndex( cc => cc.id === a.id );
const indexB = c.copyForSort.findIndex( cc => cc.id === b.id );
const substr = indexA - indexB;
if( substr != 0 ) { return substr; }
return a.title.localeCompare(b.title);
});
c.copyForSort = undefined;
}

return categories;
}

Expand Down Expand Up @@ -503,50 +547,4 @@ export async function getCategoryFolder(category) {
"flags.world-anvil.categoryId": category.id
});
}

/* -------------------------------------------- */

/**
* Recursively build a branch of the category tree.
* @param {Category} parent A parent category
* @param {Category[]} categories Categories which have not yet been allocated to a parent
* @param {number} _depth Recursive overflow protection
* @returns {Category[]} Categories which still have not been allocated to a parent
* @private
*/
function _buildCategoryBranch(parent, categories, _depth=0) {
if ( _depth > 1000 ) throw new Error("Recursive category depth exceeded. Something went wrong!");
_depth++;

// Allocate pending categories which have this parent category
let [pending, children] = categories.partition(c => {
let parentId = c.parent_category?.id;
if ( !parentId && (c.id !== CATEGORY_ID.root) ) parentId = CATEGORY_ID.root;
return parentId === parent.id;
});
children.forEach(c => c.parent = parent);
children.sort(_sortCategories);
parent["children"] = children;

// Recursively build child branches
for ( let c of children ) {
pending = _buildCategoryBranch(c, pending, _depth);
}
return pending;
}

/* -------------------------------------------- */

/**
* A comparison function for sorting categories
* @param {Category} a The first category
* @param {Category} b The second category
* @returns {number} The comparison between the two
* @private
*/
function _sortCategories(a, b) {
if ( Number.isNumeric(a.position) && Number.isNumeric(b.position) ) return a.position - b.position;
return a.title.localeCompare(b.title);
}

/* -------------------------------------------- */
14 changes: 14 additions & 0 deletions module/journal.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export default class WorldAnvilBrowser extends Application {
switch (action) {

// Header control buttons
case "refresh-all":
return this._refreshAll();
case "import-all":
return this._importCategory(this.tree);
case "sync-all":
Expand Down Expand Up @@ -214,6 +216,18 @@ export default class WorldAnvilBrowser extends Application {

/* -------------------------------------------- */

/**
* Call WA to refreesh the categoriesand the articles.
* Category tree will be rebuild when render() is called
*/
async _refreshAll() {
await getCategories({cache: false});
this.articles = undefined;
this.render();
}

/* -------------------------------------------- */

/**
* Fully link a category by creating a Folder and importing all its contained articles.
* @param {string} categoryId World Anvil category ID
Expand Down
3 changes: 3 additions & 0 deletions templates/journal.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@

<section class="world-articles">
<nav class="article-filters flexrow">
<button type="button" class="world-anvil-control" data-action="refresh-all">
<i class="fas fa-history fa-fw"></i> {{localize "WA.ButtonRefreshAll"}}
</button>
<button type="button" class="world-anvil-control" data-action="import-all">
<i class="fas fa-file-import fa-fw"></i> {{localize "WA.ButtonImportAll"}}
</button>
Expand Down
24 changes: 24 additions & 0 deletions wa.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ button#world-anvil img {
width: auto;
}


.wa-link {
padding: 1px 4px 1px 18px;
margin: 0;
white-space: nowrap;
word-break: break-all;
background: #DDD;
background-image: url(icons/wa-icon.svg);
background-repeat: no-repeat;
background-size: 14px 14px;
background-position: 2px 1px;
border: 1px solid #444;
border-radius: 2px;
}
.wa-link.not-found {
color: darkred;
}
.wa-link:hover {
text-shadow: 0 0 4px red;
cursor: pointer;
}



/** --------------------------------------------
Application Styling Rules
--------------------------------------------- */
Expand Down
2 changes: 1 addition & 1 deletion wa.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Hooks.on("renderJournalSheet", (app, html, data) => {
// View an existing linked article (OBSERVER+)
const entry = game.journal.find(e => e.getFlag("world-anvil", "articleId") === articleId);
if ( entry ) {
if ( !entry.hasPerm(game.user, "OBSERVER") ) {
if ( !entry.testUserPermission(game.user, "OBSERVER") ) {
return ui.notifications.warn(game.i18n.localize("WA.NoPermissionView"));
}
return entry.sheet.render(true);
Expand Down

0 comments on commit 7b8529e

Please sign in to comment.