# Κατασκευή ενός Remote Markdown Fetcher για Markdig

<!--category-- Markdown, AI-Article,  MarkDig, ASP.NET Core, C#, API, Nuget, FetchExtension-->
<datetime class="hidden">2025-11-07T10:00</datetime>

# Εισαγωγή

Μία από τις προκλήσεις που αντιμετώπισα κατά την κατασκευή αυτού του blog ήταν πώς να συμπεριλάβει αποτελεσματικά το εξωτερικό περιεχόμενο markdown χωρίς να αντιγράφει με το χέρι και να επικολλά παντού.

Ήθελα να φέρω αρχεία README από τα αρχεία GitHub μου, να περιλαμβάνει τεκμηρίωση από άλλα έργα, και να κρατήσει τα πάντα αυτόματα συγχρονισμένα.`Mostlylucid.Markdig.FetchExtension`Η λύση;

> Ένα έθιμο επέκταση Markdig που fitches απομακρυσμένο markdown κατά την απόδοση του χρόνου και caches την έξυπνα.

> Σε αυτή τη θέση, θα σας καθοδηγήσω στο πώς έφτιαξα

> **- μια ολοκληρωμένη λύση για τη λήψη και αποθήκευση απομακρυσμένου περιεχομένου μαρκαδόρου με υποστήριξη για πολλαπλά συστήματα υποστήριξης αποθήκευσης, αυτόματη δημοσκόπηση, και ένα μοτίβο μπαγιάτικης-ενώ-επαναισχύσεως caching μοτίβο.**Σημειώστε: Αυτό είναι ακόμα prelease, αλλά ήθελα να το πάρει εκεί έξω.`disable="true"`Καλή διασκέδαση, αλλά μπορεί να μην πιάσει ακόμα.

> **Αυτό το άρθρο είναι AI που παράγεται - χρησιμοποιώντας κώδικα Claude που με βοήθησε επίσης να χτίσει το χαρακτηριστικό.**ΕΝΗΜΕΡΩΣΗ`[TOC]`: Προστέθηκε

παράμετρος έτσι μπορούμε τώρα να demo τις ετικέτες σωστά χωρίς την επεξεργασία τους![UPDATE (Nov 7, 2025)](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension).

[![: Προστέθηκε χαρακτηριστικό γενιάς πίνακα περιεχομένων (TOC)!](https://img.shields.io/nuget/v/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
[![Χρήση](https://img.shields.io/nuget/dt/mostlylucid.Markdig.FetchExtension.svg)](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)

[TOC]

# στο markdown σας για να δημιουργήσετε αυτόματα έναν κλικ πίνακα περιεχομένων από τις επικεφαλίδες του εγγράφου.

Δες την πηγή γι' αυτό εδώ.

1. **στο GitHub για αυτό το site**NuGetCity name (optional, probably does not need a translation)
2. **NuGetCity name (optional, probably does not need a translation)**Γιατί το Κατασκευάζετε αυτό;
3. **Πριν βουτήξω στις τεχνικές λεπτομέρειες, επιτρέψτε μου να εξηγήσω το πρόβλημα.**Έχω αρκετά σενάρια όπου πρέπει να συμπεριλάβω το εξωτερικό περιεχόμενο markdown:
4. **Πακέτο READMEs**: Όταν γράφω για ένα πακέτο NuGet που έχω δημοσιεύσει, θέλω να συμπεριλάβω το README απευθείας από το GitHub

Τεκμηρίωση API

- : Εξωτερικό API docs που αλλάζει συχνά πρέπει να μείνει σε συγχρονισμό
- Κοινόχρηστο περιεχόμενο
- : Τεκμηρίωση που ζει σε ένα αποθετήριο αλλά πρέπει να εμφανίζεται σε πολλά σημεία
- Επιδόσεις

: I don't want to get this content on every page load - that would be slow and spatful

# Η αφελής προσέγγιση θα ήταν να χρησιμοποιήσεις έναν πελάτη HTTP για να φέρεις τον Markdown όποτε το χρειάζεσαι.

Αλλά αυτό είναι προβληματικό.

```mermaid
graph TD
    A[Markdown with fetch tags] --> B[MarkdownFetchPreprocessor]
    B --> C{Check Cache}
    C -->|Fresh| D[Return Cached Content]
    C -->|Stale/Missing| E[Fetch from Remote URL]
    E -->|Success| F[Update Cache]
    E -->|Failure| G{Has Cached?}
    G -->|Yes| H[Return Stale Cache]
    G -->|No| I[Return Error Comment]
    F --> J[Replace fetch with Content]
    D --> J
    H --> J
    I --> J
    J --> K[Processed Markdown]
    K --> L[Your Markdig Pipeline]
    L --> M[Final HTML]

```

Κάθε αίτημα χτυπάει τον απομακρυσμένο διακομιστή.**Χρόνοι φορτίου σελίδας για την καθυστέρηση του δικτύου**Δεν υπάρχει offline υποστήριξη

1. Κανένας χειρισμός παροδικών αποτυχιών`<fetch>`Χρειαζόμουν κάτι πιο έξυπνο: φέρε μια φορά, κρύπτη έξυπνα, αναζωογονήσου αυτόματα και χειρίσου τις αποτυχίες με χάρη.
2. Αρχιτεκτονική επισκόπηση
3. Η επέκταση ακολουθεί μια προσέγγιση προεπεξεργασίας αντί να αποτελεί μέρος του αγωγού ανάλυσης Markdig.
4. Αυτό είναι ζωτικής σημασίας επειδή σημαίνει ότι το περιεχόμενο ρέει μέσα από ολόκληρο τον αγωγό Markdig σας, λαμβάνοντας όλες τις προσαρμοσμένες επεκτάσεις σας, τονίζοντας σύνταξη, και styling.

Η βασική διορατικότητα εδώ είναι

# προεπεξεργασία



```markdown
# My Documentation

<fetch markdownurl="https://raw.githubusercontent.com/user/repo/main/README.md"
       pollfrequency="24" disable="true"/>
```

Πριν ο στόχος σας χτυπήσει τον αγωγό Markdig, εμείς:

- Σάρωση για
- ετικέτες
- Επίλυση του περιεχομένου (από κρυφή ή απομακρυσμένη)
- Αντικατάσταση των ετικετών με πραγματική markdown

# Τότε άσε τον Μαρκντίγκ να επεξεργαστεί τα πάντα μαζί.

Αυτό εξασφαλίζει συνέπεια - όλα τα markdown παίρνει την ίδια μεταχείριση ανεξάρτητα από την πηγή του.**Η βασική σύνταξη**Η χρήση της επέκτασης είναι απλή.

```mermaid
graph LR
    A[IMarkdownFetchService Interface] --> B[InMemoryMarkdownFetchService]
    A --> C[FileBasedMarkdownFetchService]
    A --> D[PostgresMarkdownFetchService]
    A --> E[SqliteMarkdownFetchService]
    A --> F[SqlServerMarkdownFetchService]
    A --> G[YourCustomService]

    B --> H[ConcurrentDictionary]
    C --> I[File System + SemaphoreSlim]
    D --> J[PostgreSQL Database]
    E --> K[SQLite Database]
    F --> L[SQL Server Database]
    G --> M[Your Storage Backend]

```

## Στο σημάδι σου:

Αυτό είναι!`IMarkdownFetchService`:

```csharp
public interface IMarkdownFetchService
{
    Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId = 0);

    Task<bool> RemoveCachedMarkdownAsync(
        string url,
        int blogPostId = 0);
}
```

Η επέκταση:

## Φέρτε το README από το GitHub

Κράτα το για 24 ώρες.`ConcurrentDictionary`:

```csharp
public class InMemoryMarkdownFetchService : IMarkdownFetchService
{
    private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<InMemoryMarkdownFetchService> _logger;

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = GetCacheKey(url, blogPostId);

        // Check cache
        if (_cache.TryGetValue(cacheKey, out var cached))
        {
            var age = DateTimeOffset.UtcNow - cached.FetchedAt;
            if (age.TotalHours < pollFrequencyHours)
            {
                _logger.LogDebug("Returning cached content for {Url}", url);
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = cached.Content
                };
            }
        }

        // Fetch fresh content
        var fetchResult = await FetchFromUrlAsync(url);

        if (fetchResult.Success)
        {
            _cache[cacheKey] = new CacheEntry
            {
                Content = fetchResult.Content,
                FetchedAt = DateTimeOffset.UtcNow
            };
        }
        else if (cached != null)
        {
            // Fetch failed, return stale cache
            _logger.LogWarning("Fetch failed, returning stale cache for {Url}", url);
            return new MarkdownFetchResult
            {
                Success = true,
                Content = cached.Content
            };
        }

        return fetchResult;
    }

    private static string GetCacheKey(string url, int blogPostId)
        => $"{url}_{blogPostId}";
}
```

Επιστρεφόμενο περιεχόμενο για τις επόμενες αιτήσεις

1. Auto-ανανέωση όταν η κρύπτη λήγει
2. Αρχιτεκτονική Προμηθευτή Αποθήκευσης
3. Μια από τις αρχές σχεδιασμού που ακολούθησα ήταν
4. ευελιξία
5. 
6. Οι διαφορετικές εφαρμογές έχουν διαφορετικές ανάγκες.
7. Μια μικρή εφαρμογή demo δεν χρειάζεται PostgreSQL, αλλά μια πολύ-server ανάπτυξη παραγωγής χρειάζεται.

Έτσι έφτιαξα μια pluggable αρχιτεκτονική αποθήκευσης:**Η βασική διεπαφή**Όλα τα εργαλεία

## Απλό και καθαρό.

Κάθε υλοποίηση χειρίζεται την αποθήκευση με τον δικό της τρόπο, αλλά η διεπαφή παραμένει συνεπής.

```csharp
public class FileBasedMarkdownFetchService : IMarkdownFetchService
{
    private readonly string _cacheDirectory;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<FileBasedMarkdownFetchService> _logger;
    private readonly SemaphoreSlim _fileLock = new(1, 1);

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = ComputeCacheKey(url, blogPostId);
        var cacheFile = GetCacheFilePath(cacheKey);

        await _fileLock.WaitAsync();
        try
        {
            // Check if file exists and is fresh
            if (File.Exists(cacheFile))
            {
                var fileInfo = new FileInfo(cacheFile);
                var age = DateTimeOffset.UtcNow - fileInfo.LastWriteTimeUtc;

                if (age.TotalHours < pollFrequencyHours)
                {
                    var cached = await File.ReadAllTextAsync(cacheFile);
                    return new MarkdownFetchResult
                    {
                        Success = true,
                        Content = cached
                    };
                }
            }

            // Fetch fresh
            var fetchResult = await FetchFromUrlAsync(url);

            if (fetchResult.Success)
            {
                await File.WriteAllTextAsync(cacheFile, fetchResult.Content);
            }
            else if (File.Exists(cacheFile))
            {
                // Return stale on fetch failure
                var stale = await File.ReadAllTextAsync(cacheFile);
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = stale
                };
            }

            return fetchResult;
        }
        finally
        {
            _fileLock.Release();
        }
    }

    private string GetCacheFilePath(string cacheKey)
        => Path.Combine(_cacheDirectory, $"{cacheKey}.md");

    private static string ComputeCacheKey(string url, int blogPostId)
    {
        var combined = $"{url}_{blogPostId}";
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(combined);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToHexString(hash);
    }
}
```

In-Memory Αποθήκευση: Ιδανικό για Demos

1. Η απλούστερη εφαρμογή χρησιμοποιεί`SemaphoreSlim`Όπως μπορείτε να δείτε αυτό κάνει τα ακόλουθα:
2. Δημιουργεί ένα κλειδί κρυφής μνήμης από το URL και το blog post ID
3. Ελέγχουμε αν έχουμε αποθηκευμένο περιεχόμενο και αν είναι φρέσκο
4. Αν η κρύπτη είναι φρέσκια, επιστρέψτε την αμέσως.
5. Αν είναι μπαγιάτικο, προσπαθεί να φέρει φρέσκο περιεχόμενο

## Για την επιτυχία, ενημερώνει την κρυφή μνήμη

Σε αποτυχία με περιεχόμενο cached, επιστρέφει μπαγιάτικη κρύπτη (ιστορικό-ενώ-revalidate!)

```csharp
public class PostgresMarkdownFetchService : IMarkdownFetchService
{
    private readonly MarkdownCacheDbContext _dbContext;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<PostgresMarkdownFetchService> _logger;

    public async Task<MarkdownFetchResult> FetchMarkdownAsync(
        string url,
        int pollFrequencyHours,
        int blogPostId)
    {
        var cacheKey = GetCacheKey(url, blogPostId);

        // Query cache
        var cached = await _dbContext.MarkdownCache
            .FirstOrDefaultAsync(c => c.CacheKey == cacheKey);

        if (cached != null)
        {
            var age = DateTimeOffset.UtcNow - cached.LastFetchedAt;
            if (age.TotalHours < pollFrequencyHours)
            {
                return new MarkdownFetchResult
                {
                    Success = true,
                    Content = cached.Content
                };
            }
        }

        // Fetch fresh
        var fetchResult = await FetchFromUrlAsync(url);

        if (fetchResult.Success)
        {
            if (cached == null)
            {
                cached = new MarkdownCacheEntry
                {
                    CacheKey = cacheKey,
                    Url = url,
                    BlogPostId = blogPostId
                };
                _dbContext.MarkdownCache.Add(cached);
            }

            cached.Content = fetchResult.Content;
            cached.LastFetchedAt = DateTimeOffset.UtcNow;
            await _dbContext.SaveChangesAsync();
        }
        else if (cached != null)
        {
            // Return stale
            return new MarkdownFetchResult
            {
                Success = true,
                Content = cached.Content
            };
        }

        return fetchResult;
    }
}
```

Σε αποτυχία χωρίς κρύπτη, επιστρέφει σφάλμα

```sql
CREATE TABLE markdown_cache (
    id SERIAL PRIMARY KEY,
    cache_key VARCHAR(128) NOT NULL UNIQUE,
    url VARCHAR(2048) NOT NULL,
    blog_post_id INTEGER NOT NULL,
    content TEXT NOT NULL,
    last_fetched_at TIMESTAMP WITH TIME ZONE NOT NULL,
    CONSTRAINT ix_markdown_cache_cache_key UNIQUE (cache_key)
);

CREATE INDEX ix_markdown_cache_url_blog_post_id
    ON markdown_cache(url, blog_post_id);
```

Αυτό το μοτίβο...

```mermaid
graph TB
    subgraph "Load Balancer"
        LB[Load Balancer]
    end

    subgraph "Application Servers"
        A1[App Server 1<br/>FetchExtension]
        A2[App Server 2<br/>FetchExtension]
        A3[App Server 3<br/>FetchExtension]
    end

    subgraph "Shared Cache"
        PG[(PostgreSQL<br/>markdown_cache table)]
    end

    subgraph "External Content"
        R1[Remote URL 1]
        R2[Remote URL 2]
        R3[Remote URL 3]
    end

    LB --> A1
    LB --> A2
    LB --> A3

    A1 <-->|Read/Write Cache| PG
    A2 <-->|Read/Write Cache| PG
    A3 <-->|Read/Write Cache| PG

    A1 -.->|Fetch if cache miss| R1
    A2 -.->|Fetch if cache miss| R2
    A3 -.->|Fetch if cache miss| R3

```

stal- while- revalidate

# - είναι ζωτικής σημασίας για την αξιοπιστία.

Ακόμα και αν το GitHub είναι κάτω, το site σας συνεχίζει να λειτουργεί με cached περιεχόμενο.

```bash
dotnet add package mostlylucid.Markdig.FetchExtension
```

Αποθήκευση με βάση το αρχείο: απλή επιμονή

```bash
# For in-memory (demos/testing)
# Already included in base package

# For file-based storage
# Already included in base package

# For PostgreSQL
dotnet add package mostlylucid.Markdig.FetchExtension.Postgres

# For SQLite
dotnet add package mostlylucid.Markdig.FetchExtension.Sqlite

# For SQL Server
dotnet add package mostlylucid.Markdig.FetchExtension.SqlServer
```

## Για τις εφαρμογές ενός υπηρέτη, εργασίες αποθήκευσης με βάση το αρχείο μεγάλη:

Κύρια σημεία εδώ:`Program.cs`:

```csharp
using Mostlylucid.Markdig.FetchExtension;

var builder = WebApplication.CreateBuilder(args);

// Option 1: In-Memory (simplest)
builder.Services.AddInMemoryMarkdownFetch();

// Option 2: File-Based (persists across restarts)
builder.Services.AddFileBasedMarkdownFetch("./markdown-cache");

// Option 3: PostgreSQL (multi-server)
builder.Services.AddPostgresMarkdownFetch(
    builder.Configuration.GetConnectionString("MarkdownCache"));

// Option 4: SQLite (single server with DB)
builder.Services.AddSqliteMarkdownFetch("Data Source=markdown-cache.db");

// Option 5: SQL Server (enterprise)
builder.Services.AddSqlServerMarkdownFetch(
    builder.Configuration.GetConnectionString("MarkdownCache"));

var app = builder.Build();

// If using database storage, ensure schema exists
if (app.Environment.IsDevelopment())
{
    app.Services.EnsureMarkdownCacheDatabase();
}

// Configure the extension with your service provider
FetchMarkdownExtension.ConfigureServiceProvider(app.Services);

app.Run();
```

## Χρήσεις

για πρόσβαση σε αρχεία ασφαλείας νημάτων

```csharp
public class MarkdownRenderingService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly MarkdownFetchPreprocessor _preprocessor;
    private readonly MarkdownPipeline _pipeline;

    public MarkdownRenderingService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _preprocessor = new MarkdownFetchPreprocessor(serviceProvider);

        _pipeline = new MarkdownPipelineBuilder()
            .UseAdvancedExtensions()
            .UseSyntaxHighlighting()
            .UseToc()  // Add TOC support for [TOC] markers
            .UseYourCustomExtensions()
            .Build();
    }

    public string RenderMarkdown(string markdown)
    {
        // Step 1: Preprocess to handle fetch tags
        var processed = _preprocessor.Preprocess(markdown);

        // Step 2: Run through your normal Markdig pipeline
        return Markdown.ToHtml(processed, _pipeline);
    }
}
```

Hases the URL + blog post ID για να δημιουργήσετε ασφαλή ονόματα αρχείων

1. Χρησιμοποιεί χρόνο τροποποίησης αρχείων για τον προσδιορισμό της φρεσκάδας`<fetch>`Επιμένει σε όλες τις επανεκκινήσεις εφαρμογής
2. Το ίδιο μπαγιάτικο-ενώ-επαναβεβαιωτικό μοτίβο
3. Αποθήκευση βάσης δεδομένων: Παραγωγή-Έτοιμη
4. Για την ανάπτυξη της παραγωγής, ειδικά ρυθμίσεις πολλαπλών εξυπηρετητών, θέλετε μια κοινή κρύπτη.

# Εκεί έρχονται οι πάροχοι της βάσης δεδομένων:

## Το σχήμα της βάσης δεδομένων είναι απλό:

Σε μια πολύ-server ανάπτυξη, αυτό σας δίνει τη συνέπεια κρύπτη σε όλες τις περιπτώσεις:**Όλοι οι σέρβερ μοιράζονται την ίδια κρύπτη.**Όταν ο Server 1 πατήσει ένα README, οι Servers 2 και 3 επωφελούνται αμέσως από αυτό το περιεχόμενο.

**Ρύθμιση της επέκτασης**

Το να ξεκινήσεις είναι ξεκάθαρο.`[TOC]`Πρώτα, εγκαταστήστε το βασικό πακέτο:

```markdown
# My Document

[TOC]

# Introduction
Content here...

# Getting Started
More content...

## Installation
Details...
```

Στη συνέχεια, επιλέξτε τον παροχέα αποθήκευσης σας:

```html
<nav class="ml_toc" aria-label="Table of Contents">
  <ul>
    <li><a href="#introduction">Introduction</a></li>
    <li><a href="#getting-started">Getting Started</a>
      <ul>
        <li><a href="#installation">Installation</a></li>
      </ul>
    </li>
  </ul>
</nav>
```

**Ρύθμιση στο πυρήνα ASP.NET**

Σε σας

```markdown
[TOC cssclass="my-custom-toc"]
```

Ενσωμάτωση με το Addown Rendering σας

```html
<nav class="my-custom-toc" aria-label="Table of Contents">
  <!-- TOC content -->
</nav>
```

**Το κλειδί είναι το βήμα προεπεξεργασίας.**

1. **Εδώ είναι πώς το ενσωματώνω στο blog μου:**Η ροή είναι:

2. **Το σημάδι σου περιέχει**ετικέτες
   
   - Ο προεπεξεργαστής τους επιλύει σε πραγματικό μαρκαδόρο.`id="getting-started"`
   - Το συνδυασμένο markdown περνάει από Markdig`id="api-reference"`

3. **Τα πάντα παίρνει έθιμο extensions σας, styling, κλπ.**Προχωρημένα χαρακτηριστικά`ul/li`Πίνακας Περιεχομένων

**Το πακέτο περιλαμβάνει τώρα ένα**

χωριστά

```csharp
var pipeline = new MarkdownPipelineBuilder()
    .UseAdvancedExtensions()
    .UseToc()  // Add TOC support - position in pipeline doesn't matter!
    .Use<YourOtherExtensions>()
    .Build();
```

**Επέκταση πίνακα περιεχομένων (TOC)!**Ενώ είναι συσκευασμένο παράλληλα με την προέκταση, είναι εντελώς ανεξάρτητο και μπορεί να χρησιμοποιηθεί μόνο του.**Μπορείτε αυτόματα να δημιουργήσετε έναν πίνακα περιεχομένων με κλικ από τις επικεφαλίδες του εγγράφου σας.**Βασική χρήση:

- Απλά προσθέστε
- οπουδήποτε στο σήμα σας:

Αυτό δημιουργεί μια λίστα με όλες τις επικεφαλίδες με συνδέσμους άγκυρας:`.UseToc()`Προσαρμοσμένες κατηγορίες CSS:

**Μπορείτε να καθορίσετε μια προσαρμοσμένη κατηγορία CSS για στυλ:**Αυτό αποδίδει με την προσαρμοσμένη τάξη σας:

- Πώς Λειτουργεί:
- Αυτόματη προστασία
- : Το TOC ανιχνεύει αυτόματα το ελάχιστο επίπεδο επικεφαλίδας στο έγγραφό σας και προσαρμόζεται ανάλογα.

**Εάν το έγγραφό σας ξεκινά με H2, το TOC αντιμετωπίζει το H2 ως το πάνω επίπεδο.**ID Generation`[TOC]`: Οι επικεφαλίδες λαμβάνουν αυτόματα ταυτότητες για τη σύνδεση άγκυρας:`.UseToc()`"Ξεκινώντας" →

## "API Reference" →

Φωτεινή δομή

```markdown
<fetch markdownurl="https://raw.githubusercontent.com/user/repo/main/docs/README.md"
       pollfrequency="24"
       transformlinks="true" disable="true"/>
```

: Ο αρθρογράφος φτιάχνει ένα σωστά φτιαγμένο

- `./CONTRIBUTING.md` → `https://github.com/user/repo/blob/main/docs/CONTRIBUTING.md`
- `../images/logo.png` → `https://github.com/user/repo/blob/main/images/logo.png`
- δομή που αντανακλά την ιεραρχία του εγγράφου σας.

Ενεργοποίηση υποστήριξης TOC:

```csharp
public class MarkdownLinkRewriter
{
    public static string RewriteLinks(string markdown, string sourceUrl)
    {
        var document = Markdown.Parse(markdown);
        var baseUri = GetBaseUri(sourceUrl);

        foreach (var link in document.Descendants<LinkInline>())
        {
            if (IsRelativeLink(link.Url))
            {
                link.Url = ResolveRelativeLink(baseUri, link.Url);
            }
        }

        using var writer = new StringWriter();
        var renderer = new NormalizeRenderer(writer);
        renderer.Render(document);
        return writer.ToString();
    }

    private static bool IsRelativeLink(string url)
    {
        if (string.IsNullOrEmpty(url)) return false;
        if (url.StartsWith("http://") || url.StartsWith("https://")) return false;
        if (url.StartsWith("#")) return false;  // Anchor
        if (url.StartsWith("mailto:")) return false;
        return true;
    }
}
```

## Κατά τη ρύθμιση του αγωγού Markdig, προσθέστε την επέκταση TOC:

Θέση αγωγών:

```markdown
<fetch markdownurl="https://api.example.com/status.md"
       pollfrequency="1"
       showsummary="true" disable="true"/>
```

Σε αντίθεση με κάποιες επεκτάσεις Markdig, η επέκταση TOC

> _Δεν με νοιάζει πού το προσθέτεις.[στον αγωγό.](https://api.example.com/status.md)Η επέκταση αυτόματα:_

Εισάγει το parser του στην αρχή της λίστας parser (θέση 0)

```markdown
<fetch markdownurl="https://example.com/docs.md"
       pollfrequency="24"
       showsummary="true"
       summarytemplate="Last updated: {retrieved:long} | Status: {status} | Next refresh: {nextrefresh:relative}" disable="true"/>
```

Ενοικιαστές μετά από όλη την ανάλυση είναι πλήρης, συλλογή επικεφαλίδων από ολόκληρο το έγγραφο

> Έτσι, μπορείτε να προσθέσετε

οπουδήποτε - αρχή, μέση ή τέλος της διαμόρφωσης του αγωγού σας.

- `{retrieved:format}`Σημαντικό:
- `{age}`Η επέκταση TOC είναι εντελώς ανεξάρτητη από την επέκταση getch.
- `{url}`Είναι απλά συσκευασμένα μαζί για ευκολία.
- `{nextrefresh:format}`Μπορείς:
- `{pollfrequency}`Χρήση TOC χωρίς λήψη
- `{status}`Χρήση πιάνω χωρίς TOC

## Χρήση και των δύο μαζί

Σημείωση:`disable="true"`Ο δείκτης TOC λειτουργεί τόσο στα κύρια αρχεία σας markdown όσο και στο απομονωμένο περιεχόμενο.

```markdown
<!-- This will be processed and fetch content -->
<fetch markdownurl="https://example.com/README.md" pollfrequency="24"/>

<!-- This will NOT be processed - useful for documentation -->
<fetch markdownurl="https://example.com/README.md" pollfrequency="24" disable="true"/>
```

Εάν πάρετε ένα README από το GitHub που περιέχει

- , θα δημιουργήσει αυτόματα έναν πίνακα περιεχομένων από τις επικεφαλίδες του εγγράφου αυτού (αν υποθέσουμε ότι έχετε προσθέσει
- στον αγωγό σας).
- Μετασχηματισμός συνδέσμων

Κατά τη λήψη απομακρυσμένου markdown (ιδιαίτερα από το GitHub), σχετικές συνδέσεις σπάνε.`<fetch>`Η επέκταση μπορεί αυτόματα να τις ξαναγράψει:`<fetch-summary>`Αυτό μεταμορφώνεται:

```markdown
<fetch-summary url="https://example.com/api/status.md" disable="true"/>
```

## Διατηρεί απόλυτο URL και άγκυρες

Η εφαρμογή χρησιμοποιεί το Markdig AST για να ξαναγράψει συνδέσμους:

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddPostgresMarkdownFetch(connectionString);

        var sp = services.BuildServiceProvider();
        var eventPublisher = sp.GetRequiredService<IMarkdownFetchEventPublisher>();

        // Subscribe to events
        eventPublisher.FetchBeginning += (sender, args) =>
        {
            Console.WriteLine($"Fetching {args.Url}...");
        };

        eventPublisher.FetchCompleted += (sender, args) =>
        {
            var source = args.WasCached ? "cache" : "remote";
            Console.WriteLine($"Fetched {args.Url} from {source} in {args.Duration.TotalMilliseconds}ms");
        };

        eventPublisher.FetchFailed += (sender, args) =>
        {
            Console.WriteLine($"Failed to fetch {args.Url}: {args.ErrorMessage}");
        };
    }
}
```

Ανακεφαλαίωση Μεταδεδομένα

```mermaid
sequenceDiagram
    participant MD as Markdown Processor
    participant EP as Event Publisher
    participant FS as Fetch Service
    participant ST as Storage Backend
    participant L as Your Listeners

    MD->>EP: FetchBeginning
    EP->>L: Notify FetchBeginning
    EP->>FS: FetchMarkdownAsync(url)
    FS->>ST: Check Cache
    alt Cache Fresh
        ST-->>FS: Cached Content
        FS->>EP: FetchCompleted (cached=true)
    else Cache Stale/Missing
        FS->>FS: HTTP GET
        alt Success
            FS->>ST: Update Cache
            ST-->>FS: OK
            FS->>EP: FetchCompleted (cached=false)
        else Failure
            FS->>EP: FetchFailed
            EP->>L: Notify FetchFailed
        end
    end
    EP->>L: Notify FetchCompleted
    EP->>L: Notify ContentUpdated
    FS-->>MD: MarkdownFetchResult

    Note over L: Listeners can be: Logging, Metrics, Telemetry, Webhooks
```

# Μπορείτε να δείξετε στους αναγνώστες όταν το περιεχόμενο πήρε για τελευταία φορά:

Αυτό αποδίδεται με ένα υποβρύχιο:

```mermaid
stateDiagram-v2
    [*] --> CheckCache: Fetch Request

    CheckCache --> Fresh: Cache exists & age < pollFrequency
    CheckCache --> Stale: Cache exists & age >= pollFrequency
    CheckCache --> Missing: No cache entry

    Fresh --> ReturnCached: Return cached content
    ReturnCached --> [*]

    Stale --> FetchRemote: Attempt HTTP GET
    Missing --> FetchRemote: Attempt HTTP GET

    FetchRemote --> UpdateCache: Success
    FetchRemote --> HasStale: Failure

    UpdateCache --> ReturnFresh: Return new content
    ReturnFresh --> [*]

    HasStale --> ReturnStale: Return stale cache
    HasStale --> ReturnError: No cache available

    ReturnStale --> [*]
    ReturnError --> [*]

    note right of Fresh
        pollFrequency = 0
        means always stale
    end note

    note right of HasStale
        Stale-while-revalidate
        pattern ensures uptime
    end note
```

Περιεχόμενο που έχει ληφθεί από

1. **https://api.example.com/status.md**στις 06 Jan 2025 (2 ώρες πριν)
2. **Ή προσαρμόστε το πρότυπο:**Έξοδος:~~~~
3. **Τελευταία ενημέρωση: 06 Ιανουάριος 2025 14:30 Κατάσταση: cached &gt; Επόμενη ανανέωση: σε 22 ώρες**Διαθέσιμοι κάτοχοι:
4. **-Τελευταία ημερομηνία/ώρα**- Ανθρώπινο-διαγνωστό χρόνο από την ανάληψη

- URL πηγής**- Όταν το περιεχόμενο θα αναζωογονηθεί**- Διάρκεια αποθήκευσης σε ώρες

# - Κατάσταση Cache (φρέσκια/συγκολλημένη/πορτοκαλί)

Απενεργοποίηση της επεξεργασίας τεκμηρίωσης

```csharp
public class MarkdownController : Controller
{
    private readonly IMarkdownFetchService _fetchService;

    public async Task<IActionResult> InvalidateCache(string url)
    {
        var removed = await _fetchService.RemoveCachedMarkdownAsync(url);

        if (removed)
        {
            return Ok(new { message = "Cache invalidated" });
        }

        return NotFound(new { message = "No cache entry found" });
    }
}
```

Όταν γράφετε τα έγγραφα σχετικά με την επέκταση (όπως αυτό το άρθρο!), χρειάζεστε έναν τρόπο για να δείξετε τις ετικέτες χωρίς να τις επεξεργαστείτε.

```csharp
// GitHub webhook notifies of README update
app.MapPost("/webhooks/github", async (
    GitHubWebhookPayload payload,
    IMarkdownFetchService fetchService) =>
{
    if (payload.Repository?.FullName == "user/repo" &&
        payload.Commits?.Any(c => c.Modified?.Contains("README.md") == true) == true)
    {
        var url = "https://raw.githubusercontent.com/user/repo/main/README.md";
        await fetchService.RemoveCachedMarkdownAsync(url);

        return Results.Ok(new { message = "Cache invalidated" });
    }

    return Results.Ok(new { message = "No action needed" });
});
```

# Χρήση του

χαρακτηριστικό:

```csharp
public class MarkdownFetchServiceTests
{
    [Fact]
    public async Task FetchMarkdownAsync_CachesContent()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddInMemoryMarkdownFetch();
        var sp = services.BuildServiceProvider();

        var fetchService = sp.GetRequiredService<IMarkdownFetchService>();
        var url = "https://raw.githubusercontent.com/user/repo/main/README.md";

        // Act - First fetch (from network)
        var result1 = await fetchService.FetchMarkdownAsync(url, 24, 0);

        // Act - Second fetch (from cache)
        var result2 = await fetchService.FetchMarkdownAsync(url, 24, 0);

        // Assert
        Assert.True(result1.Success);
        Assert.True(result2.Success);
        Assert.Equal(result1.Content, result2.Content);
    }

    [Fact]
    public async Task FetchMarkdownAsync_ReturnsStaleOnFailure()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddInMemoryMarkdownFetch();
        var sp = services.BuildServiceProvider();

        var fetchService = sp.GetRequiredService<IMarkdownFetchService>();
        var url = "https://httpstat.us/200?sleep=100";

        // Act - First fetch succeeds
        var result1 = await fetchService.FetchMarkdownAsync(url, 0, 0);

        // Change URL to fail
        var badUrl = "https://httpstat.us/500";

        // Act - Second fetch fails, should return stale
        var result2 = await fetchService.FetchMarkdownAsync(badUrl, 0, 0);

        // Assert
        Assert.True(result1.Success);
        // Even though fetch failed, we return success with stale content
        Assert.True(result2.Success);
    }
}
```

# Η ετικέτα απενεργοποίησης παραμένει στο σήμα as-is, ιδανική για:

Γράφοντας έγγραφα σχετικά με την ίδια την επέκταση

1. **Δημιουργία παραδειγμάτων σε tutorials**Εμφάνιση σύνταξης ετικετών χωρίς ενεργοποίηση φετίχ`ConcurrentDictionary`Αυτό δουλεύει και για τα δύο.
2. **και**ετικέτες:
3. **Σύστημα παρακολούθησης συμβάντων**Η επέκταση δημοσιεύει εκδηλώσεις για όλες τις επιχειρήσεις:
4. **Αυτό καθιστά εύκολο να ενσωματωθεί με Application Insights, Prometheus, ή την υποδομή καταγραφής σας:**Στρατηγική σύλληψης σε λεπτομέρειες`IHttpClientFactory`Η συμπεριφορά caching ακολουθεί μια κατάσταση μοτίβο μηχανή:
5. **Οι βασικές ιδέες εδώ:**Φρέσκα Cache

- Επιστρέψτε αμέσως, δεν χτύπησε το δίκτυο.

- Κρύσταλλο
- - Προσπάθησε να φέρεις φρέσκο, αλλά γύρνα πίσω στην μπαγιάτικη αν αποτύχει το πιάσιμο.
- Λείπει η Κέιτσε.
- - Πρέπει να φέρετε ή να επιστρέψετε το σφάλμα

# Μηδενική συχνότητα Poll

- Πάντα να φέρνετε φρέσκα (χρήσιμα για δοκιμές)

```yaml
name: Publish Markdig.FetchExtension

on:
  push:
    tags:
      - 'fetchextension-v*.*.*'

permissions:
  id-token: write
  contents: read

jobs:
  build-and-publish:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '9.0.x'

    - name: Extract version from tag
      id: get_version
      run: |
        TAG=${GITHUB_REF#refs/tags/fetchextension-v}
        echo "VERSION=$TAG" >> $GITHUB_OUTPUT

    - name: Build
      run: dotnet build Mostlylucid.Markdig.FetchExtension/Mostlylucid.Markdig.FetchExtension.csproj --configuration Release -p:Version=${{ steps.get_version.outputs.VERSION }}

    - name: Pack
      run: dotnet pack Mostlylucid.Markdig.FetchExtension/Mostlylucid.Markdig.FetchExtension.csproj --configuration Release --no-build -p:PackageVersion=${{ steps.get_version.outputs.VERSION }} --output ./artifacts

    - name: Login to NuGet (OIDC)
      id: nuget_login
      uses: NuGet/login@v1
      with:
        user: 'mostlylucid'

    - name: Publish to NuGet
      run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget_login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
```

Αυτό το μοτίβο ονομάζεται

# stal- while- revalidate

Και είναι εξαιρετικό για την αξιοπιστία.

```markdown
# My NuGet Package Documentation

Here's the official README from GitHub:

<fetch markdownurl="https://raw.githubusercontent.com/scottgal/mostlylucidweb/main/Umami.Net/README.md"
       pollfrequency="24"
       transformlinks="true"
       showsummary="true"
       summarytemplate="*Fetched {age} from GitHub*" disable="true"/>

# Installation

The package is available on NuGet...
```

Ακόμα και αν η πηγή σας πέσει, το site σας εξακολουθεί να σερβίρει cached περιεχόμενο.

# Απομάκρυνση και διαχείριση Cache

Μερικές φορές πρέπει να ακυρώσετε χειροκίνητα την κρύπτη:

1. **Ή μέσω webhooks όταν αλλάζει το περιεχόμενο:**Δοκιμή της επέκτασης
2. **Η επέκταση περιλαμβάνει ολοκληρωμένες δοκιμές.**Άκου πώς τα φτιάχνω:
3. **Εκτιμήσεις Απόδοσης**Η επέκταση είναι σχεδιασμένη για απόδοση:
4. **Συμπληρωματικό Λεξικό**- Χρήση ενθύμησης
5. **για ασφαλή πρόσβαση σε νήματα**SemaphoreSlim
6. **- Αρχείο-based χρησιμοποιεί async κλείδωμα για την πρόληψη των συνθηκών αγώνα**Δείκτες βάσης δεδομένων

- Όλοι οι πάροχοι βάσεων δεδομένων έχουν τα κατάλληλα ευρετήρια για τα κλειδιά μνήμης

- [Πελάτης HTTP Συγκέντρωση](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension)
- [- Χρήσεις](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Postgres)
- [για αποτελεσματική επαναχρησιμοποίηση σύνδεσης](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.Sqlite)
- [Async σε όλη τη διαδρομή](https://www.nuget.org/packages/mostlylucid.Markdig.FetchExtension.SqlServer)

- Όχι κλήσεις μπλοκαρίσματος, όλα είναι ασύγχρονα.[Τυπικοί αριθμοί επιδόσεων στον σέρβερ του σπιτιού μου:](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.Markdig.FetchExtension)

Case hit: < 1ms