@terrymooreii/sia 2.1.4 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/content.js CHANGED
@@ -247,6 +247,72 @@ marked.use({
247
247
  }
248
248
  });
249
249
 
250
+ /**
251
+ * Safely truncate markdown text without breaking inline formatting
252
+ * Avoids cutting in the middle of links, bold, italic, code, images, etc.
253
+ */
254
+ function truncateMarkdownSafely(text, maxLength) {
255
+ if (text.length <= maxLength) {
256
+ return text;
257
+ }
258
+
259
+ // Find all inline markdown element ranges to avoid cutting inside them
260
+ const inlinePatterns = [
261
+ /!\[([^\]]*)\]\([^)]*\)/g, // Images ![alt](url) - must come before links
262
+ /\[([^\]]*)\]\([^)]*\)/g, // Links [text](url)
263
+ /\[([^\]]*)\]\[[^\]]*\]/g, // Reference links [text][ref]
264
+ /\*\*([^*]+)\*\*/g, // Bold **text**
265
+ /__([^_]+)__/g, // Bold __text__
266
+ /\*([^*\n]+)\*/g, // Italic *text*
267
+ /_([^_\n]+)_/g, // Italic _text_
268
+ /`([^`]+)`/g, // Inline code `code`
269
+ /~~([^~]+)~~/g, // Strikethrough ~~text~~
270
+ ];
271
+
272
+ // Collect all ranges where inline elements exist
273
+ const ranges = [];
274
+ for (const pattern of inlinePatterns) {
275
+ let match;
276
+ // Reset lastIndex for each pattern
277
+ pattern.lastIndex = 0;
278
+ while ((match = pattern.exec(text)) !== null) {
279
+ ranges.push({ start: match.index, end: match.index + match[0].length });
280
+ }
281
+ }
282
+
283
+ // Sort ranges by start position
284
+ ranges.sort((a, b) => a.start - b.start);
285
+
286
+ // Find the best truncation point
287
+ let truncateAt = maxLength;
288
+
289
+ // Check if our target position is inside any markdown element
290
+ for (const range of ranges) {
291
+ if (truncateAt > range.start && truncateAt < range.end) {
292
+ // We're inside this element - decide whether to include it or exclude it
293
+ if (range.end <= maxLength + 50) {
294
+ // Include the whole element if it doesn't extend too far past our limit
295
+ truncateAt = range.end;
296
+ } else {
297
+ // Otherwise, truncate before this element starts
298
+ truncateAt = range.start;
299
+ }
300
+ break;
301
+ }
302
+ }
303
+
304
+ // Try to break at a word boundary for cleaner excerpts
305
+ if (truncateAt > 0) {
306
+ const lastSpace = text.lastIndexOf(' ', truncateAt);
307
+ // Only use word boundary if it's reasonably close to our target
308
+ if (lastSpace > truncateAt - 30 && lastSpace > maxLength * 0.5) {
309
+ truncateAt = lastSpace;
310
+ }
311
+ }
312
+
313
+ return text.substring(0, truncateAt).trim() + '...';
314
+ }
315
+
250
316
  /**
251
317
  * Generate a URL-friendly slug from a string
252
318
  */
@@ -321,12 +387,17 @@ export function parseContent(filePath) {
321
387
  if (!excerpt) {
322
388
  const firstParagraph = markdown.split('\n\n')[0];
323
389
  excerpt = firstParagraph.replace(/^#+\s+.+\n?/, '').trim();
324
- // Limit excerpt length
390
+ // Limit excerpt length using safe truncation that preserves markdown syntax
325
391
  if (excerpt.length > 200) {
326
- excerpt = excerpt.substring(0, 197) + '...';
392
+ excerpt = truncateMarkdownSafely(excerpt, 200);
327
393
  }
328
394
  }
329
395
 
396
+ // Create HTML version of excerpt for templates that need rendered output
397
+ let excerptHtml = marked.parse(excerpt);
398
+ // Clean up the HTML (remove wrapping <p> tags for inline use)
399
+ excerptHtml = excerptHtml.replace(/^<p>/, '').replace(/<\/p>\n?$/, '');
400
+
330
401
  // Normalize tags to array
331
402
  let tags = frontMatter.tags || [];
332
403
  if (typeof tags === 'string') {
@@ -338,6 +409,7 @@ export function parseContent(filePath) {
338
409
  slug,
339
410
  date,
340
411
  excerpt,
412
+ excerptHtml,
341
413
  tags,
342
414
  content: html,
343
415
  rawContent: markdown,
package/lib/templates.js CHANGED
@@ -12,7 +12,8 @@ const __dirname = dirname(__filename);
12
12
  function dateFilter(date, format = 'long') {
13
13
  if (!date) return '';
14
14
 
15
- const d = new Date(date);
15
+ // Handle special "now" keyword for current date/time
16
+ const d = (date === 'now') ? new Date() : new Date(date);
16
17
 
17
18
  if (isNaN(d.getTime())) return '';
18
19
 
@@ -205,7 +206,7 @@ export function createTemplateEngine(config) {
205
206
  }
206
207
 
207
208
  // Default templates from the selected theme
208
- const themeName = config.theme || 'main';
209
+ const themeName = config.theme?.name || 'main';
209
210
  const themeDir = join(__dirname, '..', 'themes', themeName);
210
211
  templatePaths.push(join(themeDir, 'layouts'));
211
212
  templatePaths.push(join(themeDir, 'includes'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrymooreii/sia",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "A simple, powerful static site generator with markdown, front matter, and Nunjucks templates",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -105,7 +105,8 @@ site:
105
105
  url: "https://example.com"
106
106
  author: "Your Name"
107
107
 
108
- theme: main # 'main' or 'minimal'
108
+ theme:
109
+ name: main # Options: main, minimal, developer, magazine
109
110
 
110
111
  input: src
111
112
  output: dist
@@ -6,13 +6,14 @@
6
6
  <title>{% if title %}{{ title }} | {% endif %}{{ site.title }}</title>
7
7
 
8
8
  {# Open Graph #}
9
+ {% set siteOrigin = site.url | replace(site.basePath, '') if site.basePath else site.url %}
9
10
  <meta property="og:title" content="{{ title or site.title }}">
10
11
  <meta property="og:description" content="{{ page.excerpt or page.description or site.description }}">
11
12
  <meta property="og:type" content="{% if page.collection == 'posts' %}article{% else %}website{% endif %}">
12
- <meta property="og:url" content="{{ site.url }}{{ page.url or ('/' | url) }}">
13
+ <meta property="og:url" content="{{ siteOrigin }}{{ page.url if page.url else ('/' | url) }}">
13
14
  <meta property="og:site_name" content="{{ site.title }}">
14
15
  {% if page.image %}
15
- <meta property="og:image" content="{{ site.url }}{{ page.image | url }}">
16
+ <meta property="og:image" content="{{ siteOrigin }}{{ page.image | url }}">
16
17
  {% endif %}
17
18
 
18
19
  {# Twitter Card #}
@@ -20,7 +21,7 @@
20
21
  <meta name="twitter:title" content="{{ title or site.title }}">
21
22
  <meta name="twitter:description" content="{{ page.excerpt or page.description or site.description }}">
22
23
  {% if page.image %}
23
- <meta name="twitter:image" content="{{ site.url }}{{ page.image | url }}">
24
+ <meta name="twitter:image" content="{{ siteOrigin }}{{ page.image | url }}">
24
25
  {% endif %}
25
26
 
26
27
  {# Article specific meta (for blog posts) #}
@@ -38,4 +39,4 @@
38
39
  <link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ '/feed.xml' | url }}">
39
40
 
40
41
  {# Canonical URL #}
41
- <link rel="canonical" href="{{ site.url }}{{ page.url or ('/' | url) }}">
42
+ <link rel="canonical" href="{{ siteOrigin }}{{ page.url if page.url else ('/' | url) }}">
@@ -0,0 +1,6 @@
1
+ {% if config.theme.showHero %}
2
+ <section class="hero-section">
3
+ <h1 class="hero-title">{{ site.title }}</h1>
4
+ <p class="hero-description">{{ site.description }}</p>
5
+ </section>
6
+ {% endif %}
@@ -20,7 +20,7 @@
20
20
  {% endif %}
21
21
 
22
22
  <h2 class="card-title">
23
- <a href="{{ post.url | url }}">{{ post.title }}</a>
23
+ <a href="{{ post.url }}">{{ post.title }}</a>
24
24
  </h2>
25
25
 
26
26
  <p class="card-excerpt">{{ post.excerpt | excerpt(150) }}</p>
@@ -2,10 +2,7 @@
2
2
 
3
3
  {% block content %}
4
4
  <div class="home-page">
5
- <section class="hero-section">
6
- <h1 class="hero-title">{{ site.title }}</h1>
7
- <p class="hero-description">{{ site.description }}</p>
8
- </section>
5
+ {% include "hero.njk" %}
9
6
 
10
7
  <section class="posts-section">
11
8
  <div class="section-header">
@@ -26,7 +23,7 @@
26
23
  {% endif %}
27
24
 
28
25
  <h3 class="card-title">
29
- <a href="{{ post.url | url }}">{{ post.title }}</a>
26
+ <a href="{{ post.url }}">{{ post.title }}</a>
30
27
  </h3>
31
28
 
32
29
  <p class="card-excerpt">{{ post.excerpt | excerpt(120) }}</p>
@@ -55,7 +52,7 @@
55
52
  <article class="note-card">
56
53
  <time class="note-time">{{ note.date | date('short') }}</time>
57
54
  <div class="note-preview">{{ note.excerpt | excerpt(150) }}</div>
58
- <a href="{{ note.url | url }}" class="note-link">Read more →</a>
55
+ <a href="{{ note.url }}" class="note-link">Read more →</a>
59
56
  </article>
60
57
  {% endfor %}
61
58
  </div>
@@ -17,7 +17,7 @@
17
17
  </time>
18
18
  <div class="timeline-body">
19
19
  <div class="note-text">{{ note.excerpt | excerpt(200) }}</div>
20
- <a href="{{ note.url | url }}" class="read-more">Continue reading →</a>
20
+ <a href="{{ note.url }}" class="read-more">Continue reading →</a>
21
21
  </div>
22
22
  {% if note.tags %}
23
23
  <div class="timeline-tags">
@@ -20,7 +20,7 @@
20
20
  {% endif %}
21
21
 
22
22
  <h2 class="card-title">
23
- <a href="{{ post.url | url }}">{{ post.title }}</a>
23
+ <a href="{{ post.url }}">{{ post.title }}</a>
24
24
  </h2>
25
25
 
26
26
  <p class="card-excerpt">{{ post.excerpt | excerpt(150) }}</p>
@@ -0,0 +1,8 @@
1
+ {% if config.theme.showHero %}
2
+ <section class="hero">
3
+ <div class="hero-content">
4
+ <h1 class="hero-title">{{ site.title }}</h1>
5
+ <p class="hero-subtitle">{{ site.description }}</p>
6
+ </div>
7
+ </section>
8
+ {% endif %}
@@ -68,7 +68,7 @@
68
68
  {% if post.url != page.url %}
69
69
  <article class="read-more-card">
70
70
  <h3 class="read-more-card-title">
71
- <a href="{{ post.url | url }}">{{ post.title }}</a>
71
+ <a href="{{ post.url }}">{{ post.title }}</a>
72
72
  </h3>
73
73
  <time class="read-more-date">{{ post.date | date('short') }}</time>
74
74
  </article>
@@ -14,7 +14,7 @@
14
14
  <a href="{{ ('/tags/' + (post.tags[0] | slug) + '/') | url }}" class="row-category">{{ post.tags[0] }}</a>
15
15
  {% endif %}
16
16
  <h2 class="row-title">
17
- <a href="{{ post.url | url }}">{{ post.title }}</a>
17
+ <a href="{{ post.url }}">{{ post.title }}</a>
18
18
  </h2>
19
19
  <p class="row-excerpt">{{ post.excerpt | excerpt(180) }}</p>
20
20
  <div class="row-meta">
@@ -1,12 +1,7 @@
1
1
  {% extends "base.njk" %}
2
2
 
3
3
  {% block content %}
4
- <section class="hero">
5
- <div class="hero-content">
6
- <h1 class="hero-title">{{ site.title }}</h1>
7
- <p class="hero-subtitle">{{ site.description }}</p>
8
- </div>
9
- </section>
4
+ {% include "hero.njk" %}
10
5
 
11
6
  {% set featuredPost = collections.posts[0] %}
12
7
  {% if featuredPost %}
@@ -14,7 +14,7 @@
14
14
  </time>
15
15
  <div class="note-content">
16
16
  <p>{{ note.excerpt | excerpt(250) }}</p>
17
- <a href="{{ note.url | url }}" class="note-read-more">Continue reading →</a>
17
+ <a href="{{ note.url }}" class="note-read-more">Continue reading →</a>
18
18
  </div>
19
19
  {% if note.tags %}
20
20
  <div class="note-item-tags">
@@ -12,7 +12,7 @@
12
12
  <article class="article-row">
13
13
  <div class="article-row-content">
14
14
  <h2 class="row-title">
15
- <a href="{{ post.url | url }}">{{ post.title }}</a>
15
+ <a href="{{ post.url }}">{{ post.title }}</a>
16
16
  </h2>
17
17
  <p class="row-excerpt">{{ post.excerpt | excerpt(180) }}</p>
18
18
  <div class="row-meta">
@@ -0,0 +1,6 @@
1
+ {% if config.theme.showHero %}
2
+ <section class="hero">
3
+ <h1 class="hero-title">{{ site.title }}</h1>
4
+ <p class="hero-description">{{ site.description }}</p>
5
+ </section>
6
+ {% endif %}
@@ -1,10 +1,7 @@
1
1
  {% extends "base.njk" %}
2
2
 
3
3
  {% block content %}
4
- <section class="hero">
5
- <h1 class="hero-title">{{ site.title }}</h1>
6
- <p class="hero-description">{{ site.description }}</p>
7
- </section>
4
+ {% include "hero.njk" %}
8
5
 
9
6
  <section class="section">
10
7
  <div class="section-header">
@@ -0,0 +1,6 @@
1
+ {% if config.theme.showHero %}
2
+ <section class="hero">
3
+ <h1 class="hero-title">{{ site.title }}</h1>
4
+ <p class="hero-description">{{ site.description }}</p>
5
+ </section>
6
+ {% endif %}
@@ -1,10 +1,7 @@
1
1
  {% extends "base.njk" %}
2
2
 
3
3
  {% block content %}
4
- <section class="hero">
5
- <h1 class="hero-title">{{ site.title }}</h1>
6
- <p class="hero-description">{{ site.description }}</p>
7
- </section>
4
+ {% include "hero.njk" %}
8
5
 
9
6
  <section class="section">
10
7
  <div class="section-header">
@@ -47,7 +44,7 @@
47
44
  <div class="notes-grid">
48
45
  {% for note in collections.notes | limit(3) %}
49
46
  <article class="note-card">
50
- <div class="note-card-content">{{ note.excerpt }}</div>
47
+ <div class="note-card-content">{{ note.excerptHtml | safe }}</div>
51
48
  <footer class="note-card-footer">
52
49
  <time datetime="{{ note.date | date('iso') }}">{{ note.date | date('full_time') }}</time>
53
50
  <a href="{{ note.url }}" class="note-card-link">View →</a>