Appearance
Creating Sources
Create JavaScript plugins to connect Ficnest to web novel platforms.
Easy to Build
Ficnest's source system requires only basic JavaScript knowledge (variables, functions, regex). No TypeScript, build systems, or complex tooling required.
What You Need
Your JavaScript file must define:
- SourceMetadata - Information about your source
- NovelSource - Functions that fetch content
Basic Template
javascript
// SOURCE METADATA
var SourceMetadata = {
id: "mywebsite",
name: "My Novel Website",
baseURL: "https://example.com",
version: "1.0.0",
language: "en",
contentRating: "all",
iconURL: "https://example.com/icon.png",
description: "Read novels from My Website",
author: "Your Name",
capabilities: {
popular: true,
search: true,
latest: false,
filters: false
}
};
// NOVEL SOURCE OBJECT
var NovelSource = {
baseUrl: "https://example.com",
getPopularNovels: function(page) {
var url = this.baseUrl + "/popular?page=" + page;
var response = fetch(url);
if (!response || response.error) {
return [];
}
var html = response.text;
var novels = [];
// Parse HTML and extract novels
return novels;
},
searchNovels: function(query, page) {
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
var response = fetch(url);
if (!response || response.error) {
return [];
}
// Parse and return results
return [];
},
getChapterList: function(novelId) {
var url = this.baseUrl + "/novel/" + novelId;
var response = fetch(url);
if (!response || response.error) {
return [];
}
// Parse chapter list
return [];
},
getChapterContent: function(chapterId) {
var url = this.baseUrl + "/chapter/" + chapterId;
var response = fetch(url);
if (!response || response.error) {
return "<p>Error loading chapter</p>";
}
// Extract and clean content
return "<p>Chapter content here...</p>";
}
};Required Functions
getPopularNovels(page)
Returns popular novels for the given page.
Returns: Array of novel objects
javascript
{
novelId: "unique-id", // Required
title: "Novel Title", // Required
coverUrl: "https://...", // Optional
author: "Author Name", // Optional
description: "Description" // Optional
}searchNovels(query, page)
Returns search results.
Parameters:
query(string) - Search termpage(number) - Page number
Returns: Same structure as getPopularNovels
getChapterList(novelId)
Returns all chapters for a novel.
Returns: Array of chapter objects
javascript
{
chapterId: "unique-id", // Required
title: "Chapter Title", // Required
index: 0 // Required (chapter order)
}getChapterContent(chapterId)
Returns HTML content of a chapter.
Returns: HTML string with chapter content
Using fetch()
Ficnest's fetch works differently from browser fetch:
javascript
var response = fetch(url);
// Check for errors
if (!response || response.error) {
return [];
}
// Get content (property, not function!)
var html = response.text; // Direct accessResponse structure:
javascript
{
ok: true,
status: 200,
text: "response body",
error: null
}Parsing with Regex
Extract data from HTML using regular expressions:
javascript
// Find all matches
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;
while ((match = pattern.exec(html)) !== null) {
var novelId = match[1];
var title = match[2];
}Helper Functions
Decode HTML Entities
javascript
decodeHTML: function(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ");
}Logging
javascript
log("Fetching page " + page);
log("Found " + novels.length + " novels");Common Mistakes
Don't use ES6 syntax:
javascript
// DON'T
const NovelSource = { ... };
let myVariable = "value";
// DO
var NovelSource = { ... };
var myVariable = "value";Don't call .text() as function:
javascript
// DON'T
var html = response.text();
// DO
var html = response.text;Always encode URLs:
javascript
// DON'T
var url = baseUrl + "?q=" + query;
// DO
var url = baseUrl + "?q=" + encodeURIComponent(query);Hosting Your Source
Create manifest.json
json
{
"version": "1.0.0",
"sources": [
{
"id": "mywebsite",
"name": "My Novel Website",
"baseUrl": "https://example.com",
"version": "1.0.0",
"language": "en",
"contentRating": "all",
"scriptUrl": "https://yourserver.com/mywebsite.js",
"description": "Read novels from My Website",
"author": "Your Name"
}
]
}Upload Files
Host both files on a public server:
https://yourserver.com/
├── manifest.json
└── mywebsite.jsShare the manifest URL: https://yourserver.com/manifest.json
Testing
- Enable Developer Mode in Settings
- Import your source file
- Test each function:
- Popular novels
- Search
- Chapter list
- Chapter content
- Check console logs for errors
Testing Checklist
- [ ] Popular novels load correctly
- [ ] Search returns relevant results
- [ ] Chapters appear in correct order
- [ ] Content displays properly
- [ ] No ads or scripts in content
- [ ] Error handling works
- [ ] Console shows helpful logs
Complete Example
Click to view the full source plugin guide
Full Source Plugin Guide
Ficnest Source Plugin Guide
Welcome! This guide will help you create JavaScript plugins (sources) that work with Ficnest. Sources allow Ficnest to fetch novels and chapters from different websites.
Note: Ficnest's source system is designed to be significantly easier and more intuitive than similar systems like Aidoku or Paperback. If you have basic JavaScript knowledge (variables, functions, and regex), you can create a working source plugin. No complex build systems, TypeScript, or advanced tooling required—just plain JavaScript!
What You Need
Your JavaScript file needs to define two things:
- SourceMetadata - Information about your source
- NovelSource - Functions that fetch content from the website
Basic Template
Here's a minimal working template you can start with:
javascript
// ================================
// SOURCE METADATA
// ================================
var SourceMetadata = {
id: "mywebsite", // Unique ID (lowercase, no spaces)
name: "My Novel Website", // Display name
baseURL: "https://example.com", // Website URL
version: "1.0.0", // Your plugin version
language: "en", // Language code (en, es, fr, etc.)
contentRating: "all", // all, teen, mature, adult
// Optional fields:
iconURL: "https://example.com/icon.png",
description: "Read novels from My Website",
author: "Your Name",
capabilities: {
popular: true, // Can get popular novels?
search: true, // Can search novels?
latest: false, // Can get latest updates?
filters: false // Has custom filters?
}
};
// ================================
// NOVEL SOURCE OBJECT
// ================================
var NovelSource = {
baseUrl: "https://example.com",
// Required: Get popular novels
getPopularNovels: function(page) {
log("Fetching page " + page);
var url = this.baseUrl + "/popular?page=" + page;
var response = fetch(url);
// Always check for errors!
if (!response || response.error) {
log("Error: " + (response ? response.error : "No response"));
return [];
}
var html = response.text;
var novels = [];
// Parse HTML and extract novels
// ... your parsing code here ...
return novels;
},
// Required: Search for novels
searchNovels: function(query, page) {
log("Searching: " + query);
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
var response = fetch(url);
if (!response || response.error) {
return [];
}
var html = response.text;
var novels = [];
// Parse and return results
return novels;
},
// Required: Get chapter list for a novel
getChapterList: function(novelId) {
log("Getting chapters for: " + novelId);
var url = this.baseUrl + "/novel/" + novelId;
var response = fetch(url);
if (!response || response.error) {
return [];
}
var html = response.text;
var chapters = [];
// Parse chapter list
return chapters;
},
// Required: Get chapter content (as HTML)
getChapterContent: function(chapterId) {
log("Getting content for: " + chapterId);
var url = this.baseUrl + "/chapter/" + chapterId;
var response = fetch(url);
if (!response || response.error) {
return "<p>Error loading chapter</p>";
}
var html = response.text;
// Extract and clean content
return "<p>Chapter content here...</p>";
}
};Required Functions
Your NovelSource object must implement these four functions:
1. getPopularNovels(page)
Returns an array of popular novels for the given page number.
Parameters:
page(number) - The page number (starts at 1)
Returns: Array of objects with this structure:
javascript
{
novelId: "unique-novel-id", // Required
title: "Novel Title", // Required
coverUrl: "https://example.com/cover.jpg", // Optional
author: "Author Name", // Optional
description: "Brief description" // Optional
}Example:
javascript
getPopularNovels: function(page) {
var url = this.baseUrl + "/popular?page=" + page;
var response = fetch(url);
if (!response || response.error || !response.ok) {
return [];
}
var novels = [];
var html = response.text;
// Use regex to find novel links
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;
while ((match = pattern.exec(html)) !== null) {
novels.push({
novelId: match[1],
title: match[2],
coverUrl: "",
author: "",
description: ""
});
}
return novels;
}2. searchNovels(query, page)
Returns search results for the given query.
Parameters:
query(string) - The search termpage(number) - The page number
Returns: Same structure as getPopularNovels
Example:
javascript
searchNovels: function(query, page) {
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);
var response = fetch(url);
if (!response || response.error) {
return [];
}
// Parse search results (same format as popular novels)
return novels;
}3. getChapterList(novelId)
Returns all chapters for a specific novel.
Parameters:
novelId(string) - The unique ID of the novel (fromgetPopularNovelsorsearchNovels)
Returns: Array of chapter objects:
javascript
{
chapterId: "unique-chapter-id", // Required
title: "Chapter 1: Beginning", // Required
index: 0 // Required (chapter order, starts at 0)
}Example:
javascript
getChapterList: function(novelId) {
var url = this.baseUrl + "/novel/" + novelId + "/chapters";
var response = fetch(url);
if (!response || response.error) {
return [];
}
var chapters = [];
var html = response.text;
var index = 0;
var pattern = /<a href="\/chapter\/([^"]+)">([^<]+)<\/a>/g;
var match;
while ((match = pattern.exec(html)) !== null) {
chapters.push({
chapterId: match[1],
title: match[2],
index: index
});
index++;
}
return chapters;
}4. getChapterContent(chapterId)
Returns the HTML content of a specific chapter.
Parameters:
chapterId(string) - The unique ID of the chapter (fromgetChapterList)
Returns: HTML string with the chapter content (wrapped in <p> tags)
Example:
javascript
getChapterContent: function(chapterId) {
var url = this.baseUrl + "/chapter/" + chapterId;
var response = fetch(url);
if (!response || response.error) {
return "<p>Error loading chapter</p>";
}
var html = response.text;
// Extract content between markers
var startMarker = '<div class="chapter-content">';
var endMarker = '</div>';
var startIdx = html.indexOf(startMarker);
var endIdx = html.indexOf(endMarker, startIdx);
if (startIdx === -1 || endIdx === -1) {
return "<p>Could not extract chapter content</p>";
}
var content = html.substring(startIdx, endIdx);
// Clean up HTML
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
return content;
}Using the fetch Function
Ficnest provides a custom fetch function that works differently from browser fetch:
Correct Usage:
javascript
var response = fetch(url);
// Always check for errors first
if (!response || response.error) {
log("Error: " + (response ? response.error : "Network error"));
return [];
}
// Check HTTP status
if (!response.ok) {
log("HTTP Error: " + response.status);
return [];
}
// Get the content as a string (NOT a function call!)
var html = response.text; // Direct property accessDon't Do This:
javascript
// WRONG - This is browser syntax, not for Ficnest
var html = response.text(); // This will cause errors!
// WRONG - Using await/async (not supported)
var response = await fetch(url);
// WRONG - Using .json() method
var data = response.json();Response Object Structure:
javascript
{
ok: true, // Boolean: true if status 200-299
status: 200, // HTTP status code
text: "...", // Response body as string
error: null // Error message if request failed
}
// On error:
{
error: "Error message"
}Helper Functions & Best Practices
Use Regular Expressions for Parsing
JavaScript regex is powerful for extracting data from HTML:
javascript
// Find all matches
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;
while ((match = pattern.exec(html)) !== null) {
var novelId = match[1];
var title = match[2];
// Use the data...
}
// Find first match
var singleMatch = /<div class="author">([^<]+)<\/div>/.exec(html);
if (singleMatch) {
var author = singleMatch[1];
}Clean HTML Entities
Create a helper function to decode HTML entities:
javascript
decodeHTML: function(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ");
}Extract Text Between Markers
javascript
extractBetween: function(text, start, end) {
var startIndex = text.indexOf(start);
if (startIndex === -1) return "";
var textAfterStart = text.substring(startIndex + start.length);
var endIndex = textAfterStart.indexOf(end);
if (endIndex === -1) return textAfterStart;
return textAfterStart.substring(0, endIndex);
}Always Log for Debugging
Use the log() function to help debug issues:
javascript
log("Fetching page " + page);
log("Found " + novels.length + " novels");
log("Error: " + response.error);Optional: Adding Filters
If your source supports filtering (by genre, status, etc.), implement getFilters():
javascript
getFilters: function() {
return {
genre: {
type: "select",
label: "Genre",
options: [
{ value: "", label: "All" },
{ value: "fantasy", label: "Fantasy" },
{ value: "romance", label: "Romance" },
{ value: "action", label: "Action" }
],
default: ""
},
status: {
type: "select",
label: "Status",
options: [
{ value: "", label: "All" },
{ value: "ongoing", label: "Ongoing" },
{ value: "completed", label: "Completed" }
],
default: ""
}
};
}Then modify getPopularNovels to accept filters:
javascript
getPopularNovels: function(page, filters) {
var url = this.baseUrl + "/novels?page=" + page;
if (filters && filters.genre) {
url += "&genre=" + filters.genre;
}
if (filters && filters.status) {
url += "&status=" + filters.status;
}
// ... rest of the code
}Common Mistakes to Avoid
1. Using const and let
Don't use:
javascript
const NovelSource = { ... };
let myVariable = "value";Use instead:
javascript
var NovelSource = { ... };
var myVariable = "value";Why? Ficnest uses JavaScriptCore which may not support modern ES6 syntax in all cases.
2. Forgetting Error Checks
Don't do:
javascript
var response = fetch(url);
var html = response.text; // Crashes if fetch failed!Always check:
javascript
var response = fetch(url);
if (!response || response.error) {
log("Fetch failed: " + (response ? response.error : "Unknown"));
return [];
}
var html = response.text;3. Using Wrong Response Format
Don't use:
javascript
var text = response.text(); // ❌ Not a function!
var json = response.json(); // ❌ Method doesn't existUse:
javascript
var text = response.text; // Direct property access4. Returning Wrong Data Structure
Missing required fields:
javascript
return [{
title: "My Novel"
// Missing novelId!
}];Include all required fields:
javascript
return [{
novelId: "novel-123",
title: "My Novel",
coverUrl: "",
author: "",
description: ""
}];5. Not Handling URL Encoding
Don't do:
javascript
var url = this.baseUrl + "/search?q=" + query; // Breaks with spaces!Use encodeURIComponent:
javascript
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query);Debugging Tips
1. Enable Developer Mode
Always enable Developer Mode in Settings before testing your source. This gives you access to:
- Real-time console logs
- Source testing tools
- Error reporting with stack traces
- Network request inspection
2. Use Descriptive Log Messages
Add informative log() statements throughout your code:
javascript
log("Starting getPopularNovels for page " + page);
log("Fetching URL: " + url);
log("Response status: " + response.status);
log("Found " + novels.length + " novels");
log("Novel IDs: " + novels.map(n => n.novelId).join(", "));These messages will appear in the Developer Mode console, helping you track exactly what's happening.
3. Check the Console
Look for your log() messages in the Developer Mode console view. The console shows:
- Your custom log messages
- Automatic logs for network requests
- JavaScript errors with line numbers
- Function execution times
4. Test Each Function Separately
Start with getPopularNovels(1) first, then move to other functions. Developer Mode may provide testing tools to call individual functions directly.
5. Verify HTML Structure
Use your browser to view the target website's HTML and understand its structure:
- Open the website in your browser (Chrome, Firefox, Safari, Edge, etc.)
- Right-click → Inspect Element (or press F12 / ⌘+Option+I)
- Look for patterns in the HTML
- Test your regex patterns
6. Use Simple Patterns First
Start with basic regex patterns and make them more complex as needed:
javascript
// Start simple
var pattern = /<a href="([^"]+)">([^<]+)<\/a>/g;
// Add specificity if needed
var pattern = /<a href="\/novel\/([^"]+)" class="novel-link">([^<]+)<\/a>/g;7. Export Logs for Analysis
Use Developer Mode's log export feature to:
- Save logs to a file
- Share logs when asking for help
- Review logs offline to identify patterns
Installing Your Source
Option 1: Install from File
For quick testing, you can install a source directly from a JavaScript file:
- Save your JavaScript file (e.g.,
mywebsite.js) - In Ficnest, go to Sources tab
- Tap the "+" button
- Select "Import from File"
- Choose your JavaScript file
Option 2: Install from Repository (Recommended)
To share your source with others, host it on a web server and create a repository manifest. Users can then add your repository URL to automatically install and update your source.
Step 1: Create Your JavaScript Source File
Save your source plugin as a .js file (e.g., mywebsite.js).
Step 2: Create a manifest.json File
Create a manifest file that describes your source repository:
json
{
"version": "1.0.0",
"sources": [
{
"id": "mywebsite",
"name": "My Novel Website",
"baseUrl": "https://example.com",
"version": "1.0.0",
"language": "en",
"contentRating": "all",
"scriptUrl": "https://yourserver.com/path/to/mywebsite.js",
"description": "Read novels from My Website",
"author": "Your Name",
"iconURL": "https://example.com/icon.png"
}
]
}Manifest Field Descriptions:
version(string, required): The version of your repository manifestsources(array, required): Array of source objects
Source Object Fields:
id(string, required): Unique identifier for your source (lowercase, no spaces)name(string, required): Display name shown in the appbaseUrl(string, required): Base URL of the websiteversion(string, required): Version of your source pluginlanguage(string, required): Language code (e.g., "en", "es", "fr", "ja", "zh")contentRating(string, required): Content rating ("all", "teen", "mature", or "adult")scriptUrl(string, required): Direct URL to your JavaScript file (must be publicly accessible)description(string, optional): Brief description of the sourceauthor(string, optional): Your name or organizationiconURL(string, optional): URL to an icon image for the source
Step 3: Host Your Files
Upload both files to a web server or hosting service:
https://yourserver.com/
├── manifest.json
└── sources/
└── mywebsite.jsImportant: The scriptUrl in your manifest must point to the actual JavaScript file URL that Ficnest can download. Make sure:
- The URL is publicly accessible (not behind authentication)
- The URL returns the raw JavaScript file (not an HTML page)
- CORS headers allow downloads if needed
Step 4: Share Your Repository URL
Users can now add your repository by entering the manifest URL in Ficnest:
https://yourserver.com/manifest.jsonMultiple Sources in One Repository
You can include multiple sources in a single repository:
json
{
"version": "1.0.0",
"sources": [
{
"id": "website1",
"name": "Website One",
"baseUrl": "https://website1.com",
"version": "1.0.0",
"language": "en",
"contentRating": "all",
"scriptUrl": "https://yourserver.com/sources/website1.js"
},
{
"id": "website2",
"name": "Website Two",
"baseUrl": "https://website2.com",
"version": "1.2.0",
"language": "ja",
"contentRating": "teen",
"scriptUrl": "https://yourserver.com/sources/website2.js"
}
]
}Updating Your Sources
When you update your source:
- Increment the
versionfield in both your JavaScript file'sSourceMetadataand inmanifest.json - Upload the new files to your server
- Users with your repository installed will be notified of the update
Complete Example
Here's a complete, working example for a fictional website:
javascript
// ================================
// SOURCE METADATA
// ================================
var SourceMetadata = {
id: "novelsite",
name: "Novel Site",
baseURL: "https://novelsite.com",
iconURL: "https://novelsite.com/icon.png",
version: "1.0.0",
language: "en",
contentRating: "all",
description: "Read free novels online",
author: "Community",
capabilities: {
popular: true,
search: true,
latest: false,
filters: false
}
};
// ================================
// NOVEL SOURCE
// ================================
var NovelSource = {
baseUrl: "https://novelsite.com",
// Helper: Decode HTML entities
decodeHTML: function(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ");
},
// Get popular novels
getPopularNovels: function(page) {
log("Fetching popular novels, page: " + page);
var url = this.baseUrl + "/popular?page=" + page;
var response = fetch(url);
if (!response || response.error) {
log("Error: " + (response ? response.error : "Network error"));
return [];
}
if (!response.ok) {
log("HTTP Error: " + response.status);
return [];
}
var html = response.text;
var novels = [];
// Pattern: <a href="/novel/novel-slug"><img src="cover.jpg" alt="Title">
var pattern = /<a href="\/novel\/([^"]+)"><img src="([^"]+)" alt="([^"]+)"/g;
var match;
while ((match = pattern.exec(html)) !== null) {
var novelId = match[1];
var coverUrl = match[2].startsWith("http") ? match[2] : this.baseUrl + match[2];
var title = this.decodeHTML(match[3]);
novels.push({
novelId: novelId,
title: title,
coverUrl: coverUrl,
author: "",
description: ""
});
}
log("Found " + novels.length + " novels");
return novels;
},
// Search novels
searchNovels: function(query, page) {
log("Searching: " + query + ", page: " + page);
var url = this.baseUrl + "/search?q=" + encodeURIComponent(query) + "&page=" + page;
var response = fetch(url);
if (!response || response.error || !response.ok) {
return [];
}
var html = response.text;
var novels = [];
// Use same pattern as popular novels
var pattern = /<a href="\/novel\/([^"]+)"><img src="([^"]+)" alt="([^"]+)"/g;
var match;
while ((match = pattern.exec(html)) !== null) {
novels.push({
novelId: match[1],
title: this.decodeHTML(match[3]),
coverUrl: match[2].startsWith("http") ? match[2] : this.baseUrl + match[2],
author: "",
description: ""
});
}
log("Found " + novels.length + " results");
return novels;
},
// Get chapter list
getChapterList: function(novelId) {
log("Getting chapters for: " + novelId);
var url = this.baseUrl + "/novel/" + novelId;
var response = fetch(url);
if (!response || response.error || !response.ok) {
return [];
}
var html = response.text;
var chapters = [];
// Pattern: <a href="/novel/slug/chapter-1">Chapter 1: Title</a>
var pattern = /<a href="\/novel\/[^\/]+\/(chapter-\d+)">([^<]+)<\/a>/g;
var match;
var index = 0;
while ((match = pattern.exec(html)) !== null) {
var chapterSlug = match[1];
var title = this.decodeHTML(match[2]).trim();
chapters.push({
chapterId: novelId + "/" + chapterSlug,
title: title,
index: index
});
index++;
}
log("Found " + chapters.length + " chapters");
return chapters;
},
// Get chapter content
getChapterContent: function(chapterId) {
log("Getting content for: " + chapterId);
// Extract novelId and chapter from chapterId
var parts = chapterId.split("/");
if (parts.length !== 2) {
log("Invalid chapter ID format");
return "<p>Error: Invalid chapter ID</p>";
}
var novelId = parts[0];
var chapterSlug = parts[1];
var url = this.baseUrl + "/novel/" + novelId + "/" + chapterSlug;
var response = fetch(url);
if (!response || response.error || !response.ok) {
return "<p>Error loading chapter</p>";
}
var html = response.text;
// Extract content between markers
var startMarker = '<div class="chapter-content">';
var endMarker = '<div class="chapter-nav">';
var startIdx = html.indexOf(startMarker);
var endIdx = html.indexOf(endMarker, startIdx);
if (startIdx === -1) {
log("Could not find chapter content");
return "<p>Error: Could not find chapter content</p>";
}
if (endIdx === -1) {
endIdx = html.length;
}
var content = html.substring(startIdx, endIdx);
// Clean up content
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
content = content.replace(/<a[^>]+class="[^"]*button[^"]*"[^>]*>[\s\S]*?<\/a>/gi, "");
// Extract paragraphs
var paragraphPattern = /<p[^>]*>([\s\S]*?)<\/p>/gi;
var paragraphs = [];
var match;
while ((match = paragraphPattern.exec(content)) !== null) {
var pContent = match[1]
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<[^>]+>/g, "")
.trim();
pContent = this.decodeHTML(pContent);
if (pContent.length > 0) {
paragraphs.push("<p>" + pContent + "</p>");
}
}
var finalContent = paragraphs.join("\n");
if (finalContent.length < 50) {
log("Warning: Content seems too short");
return "<p>Error: Chapter content too short or parsing failed</p>";
}
log("Extracted " + finalContent.length + " characters");
return finalContent;
}
};Testing Your Source
Testing is crucial to ensure your source works correctly. Here's a comprehensive testing workflow:
Local Testing Workflow
Step 1: Create Your Source File
Save your JavaScript source as mywebsite.js (or any name you prefer).
Step 2: Import into Ficnest
- Open Ficnest
- Go to the Sources tab
- Tap the "+" button
- Select "Import from File"
- Choose your
.jsfile
Step 3: Enable the Source
After importing, make sure your source is enabled in the Sources list.
Step 4: Test Each Function
Test your source functions in this order:
Test Popular Novels
- Navigate to Browse or the source's page
- Try loading page 1
- Verify novels appear with correct titles and covers
- Try loading page 2 to test pagination
- Check console logs for any errors
Test Search
- Use the search feature
- Try different search terms
- Verify results match your search query
- Test search pagination if applicable
Test Chapter List
- Open a novel from the browse/search results
- Verify all chapters load correctly
- Check that chapter titles are properly formatted
- Ensure chapters are in the correct order
Test Chapter Content
- Open a chapter
- Verify the content displays correctly
- Check that paragraphs are properly formatted
- Ensure no ads, navigation buttons, or scripts appear
- Test multiple chapters to ensure consistency
Step 5: Check Console Logs
Throughout testing, monitor the console for:
- Your
log()statements - Any error messages
- Unexpected behavior or warnings
Debugging Console
To view console logs while testing, enable Developer Mode in Ficnest:
Enable Developer Mode:
- Open Ficnest Settings
- Scroll to "Developer" section
- Toggle "Developer Mode" on
- Return to the app
View Debug Logs:
Once Developer Mode is enabled, you can access the console:
- Console View: Tap "Open Console" in Developer settings to see real-time log output
- Source Testing: Test individual source functions directly from the Sources detail view
- Log Export: Export logs as text files for troubleshooting
All your log() statements from your source plugin will appear in the console, making it easy to troubleshoot without needing Xcode or external tools.
What Gets Logged:
When Developer Mode is active, you'll see:
- Your custom
log()messages from your source code - HTTP request URLs being fetched
- Response status codes and errors
- Function execution timing
- Parse results (number of items found, etc.)
- JavaScript errors with details
Common Issues During Testing
Problem: No novels appear
- Check if your regex pattern matches the website's HTML structure
- Verify the fetch request succeeded (
response.okshould be true) - Look for log messages indicating what went wrong
- Visit the website in your browser and inspect the HTML structure (F12 or right-click → Inspect)
Problem: Covers don't load
- Ensure
coverUrlis a complete URL (starts withhttp://orhttps://) - Check if relative URLs need to be converted to absolute URLs
- Example:
cover.jpg→https://example.com/cover.jpg
Problem: Chapter content is empty or malformed
- Verify your content extraction markers are correct
- Check if the website uses different HTML structure than expected
- Ensure you're removing all unwanted elements (scripts, ads, buttons)
- Test with multiple chapters to ensure consistency
Problem: Search returns wrong results
- Verify your search URL is correctly formatted
- Check if you're using
encodeURIComponent()for the query - Test with different search terms
- Compare with the actual website's search
Testing with Different Websites
When developing a new source:
- Open the target website in your browser
- Right-click and select "Inspect Element" (or press F12 / ⌘+Option+I)
- Find patterns in the HTML:
- Look for novel listing elements
- Identify chapter links
- Locate content containers
- Test your regex patterns:
- Use online regex testers like regex101.com
- Copy sample HTML from the website
- Test your patterns before implementing
Version Control for Testing
When making changes to your source:
- Increment the version number in
SourceMetadata - Document what changed in comments
- Re-import the updated file into Ficnest
- Re-test all functions to ensure nothing broke
Example:
javascript
var SourceMetadata = {
id: "mywebsite",
name: "My Website",
version: "1.0.1", // Increment this when you make changes
// ...
};Testing Checklist
Before sharing your source, verify:
- [ ] Enabled Developer Mode in Settings
- [ ] Popular novels load on page 1
- [ ] Pagination works (page 2, 3, etc.)
- [ ] Search returns relevant results
- [ ] Novel details page loads correctly
- [ ] Chapter list appears in correct order
- [ ] Chapter content displays properly formatted text
- [ ] No ads, scripts, or navigation buttons in content
- [ ] Covers load correctly (if available)
- [ ] Error handling works (test with invalid URLs/IDs)
- [ ] Console shows helpful log messages (check Developer Mode console)
- [ ] Source works consistently across multiple novels
- [ ] All required fields are populated (
novelId,title,chapterId, etc.) - [ ] Tested with different novels from the website
- [ ] No JavaScript errors appear in debug console
Browser-Based Pre-Testing (Optional)
You can test your regex patterns and logic before importing into Ficnest:
- Open the target website in your browser
- Open Developer Console (F12 or ⌘+Option+I)
- Paste and test your extraction code:
javascript
// Test in browser console
var html = document.body.innerHTML;
// Test your regex pattern
var pattern = /<a href="\/novel\/([^"]+)">([^<]+)<\/a>/g;
var match;
var count = 0;
while ((match = pattern.exec(html)) !== null) {
console.log("Novel ID:", match[1], "Title:", match[2]);
count++;
}
console.log("Total found:", count);This helps you validate your patterns quickly without importing into the app.
Additional Resources
Getting Help
If you run into issues:
- Review your console logs for error messages in Developer Mode
- Test your regex patterns in an online regex tester like regex101.com
- Compare your source with the complete example in this guide
- Use browser developer tools (F12) to inspect the target website's HTML structure
Summary Checklist
Before submitting your plugin, make sure:
- [ ] Used
varinstead ofconstorlet - [ ] Defined both
SourceMetadataandNovelSource - [ ] Implemented all 4 required functions
- [ ] Added error checking for all
fetch()calls - [ ] Used
response.text(notresponse.text()) - [ ] Returned correct object structures
- [ ] Added
log()statements for debugging - [ ] Tested with the actual website
- [ ] Cleaned up HTML properly
- [ ] Handled URL encoding for search queries
- [ ] Used proper chapterId format (include novelId)
- [ ] Created a
manifest.jsonfile with correctscriptUrl - [ ] Hosted both files on a publicly accessible server
- [ ] Tested installation from repository URL
Happy coding!
If you need help or have questions, refer to the existing examples in the project or check the debugging guide.
