---
title: "Importers: what a dropped file becomes"
description: "The importer registry of the feature contract: Importer and ImportPlan, planImport, the Markdown, subtitle and BibTeX importers of the media feature, and how composite-media wraps the plan."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/importers"
---
# Importers: what a dropped file becomes

> The importer registry of the feature contract: Importer and ImportPlan, planImport, the Markdown, subtitle and BibTeX importers of the media feature, and how composite-media wraps the plan.

Dropping, pasting or picking a file runs it through the **importer registry**. A feature contributes `importers: Importer[]`; the editor asks them, in feature order, which one takes the file. The result is an `ImportPlan`: the blocks to insert, plus facts about the file as a whole. `fileImport` (`@skaile/forge-editor/media`) inserts the blocks at the drop position and shows the plan's warnings as one notice.

```ts
import type { Importer } from '@skaile/forge-editor/core'

const csvImporter: Importer = {
  id: 'csv',
  accepts: ['.csv', 'text/csv'],
  async plan(editor, file, text) {
    // `text` is the file's text for a text file of at most MAX_TEXT_BYTES, else null
    return { blocks: [{ type: 'paragraph', content: [{ type: 'text', text: text ?? '' }] }] }
  },
}

export const csvFeature = (): EditorFeature<never> => ({ id: 'csv', dimension: 'content', importers: [csvImporter] })
```

## Which importer takes a file

`accepts` lists MIME types (`text/markdown`, `audio/*`), extensions with their dot (`.vtt`, not case-sensitive) or `*`.

- **Specific entries.** The first importer in feature order with a matching MIME type or extension plans the file. A file without a match goes to the **fallback**: text and Markdown files are parsed through the block pipeline (parse, normalize, segment, structure), everything else is uploaded as a media or file block.
- **Wrapping importers (*).** An importer that accepts `*` takes every file but plans last. It receives the plan made so far as `base` and returns the final plan. `composite-media` is one: it puts the blocks of every file into one Typed block.
- **Text.** An importer that matched by extension or a `text/*` type gets the file's text. Files above `MAX_TEXT_BYTES` (2 MB) are not read; such a file goes to the fallback.

Importer ids must be unique: `defineEditor` throws for a second `importers` entry with the same id. Importers are additive, so two features may both contribute some. `planImport(editor, file, fallback)` is the framework-free entry; `importersOf(features)` lists the contributions.

## The importers of the media feature

The `media` feature contributes three importers (`FILE_IMPORTERS`, in this order).

<table>
<thead>
  <tr>
    <th>
      Importer
    </th>
    
    <th>
      Takes
    </th>
    
    <th>
      Plan
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        markdown
      </code>
    </td>
    
    <td>
      <code>
        .md
      </code>
      
      , <code>
        .markdown
      </code>
      
      , <code>
        text/markdown
      </code>
    </td>
    
    <td>
      The body parsed through the block pipeline. The frontmatter keys become <code>
        properties
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subtitle
      </code>
    </td>
    
    <td>
      <code>
        .vtt
      </code>
      
      , <code>
        .srt
      </code>
      
      , <code>
        text/vtt
      </code>
    </td>
    
    <td>
      One Typed block of Block type <code>
        rendition
      </code>
      
      , titled with the file name, holding one <code>
        transcriptSegment
      </code>
      
       per cue (<code>
        start
      </code>
      
      , <code>
        end
      </code>
      
       in seconds, <code>
        speaker
      </code>
      
       from a WebVTT <code>
        <v Name>
      </code>
      
       tag)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        bibtex
      </code>
    </td>
    
    <td>
      <code>
        .bib
      </code>
      
      , <code>
        application/x-bibtex
      </code>
    </td>
    
    <td>
      One Typed block per entry: <code>
        paper
      </code>
      
      , or <code>
        book
      </code>
      
       for CSL <code>
        book
      </code>
      
      . The entry is the CSL record in <code>
        meta.bib
      </code>
      
       (<code>
        doi
      </code>
      
       / <code>
        isbn
      </code>
      
       too)
    </td>
  </tr>
</tbody>
</table>

Without the `composites` feature there are no Typed blocks: the subtitle importer then inserts the segments as they are (paragraphs `Sprecher: Text` without the transcript feature), the BibTeX importer one citation paragraph per entry.

**Frontmatter** is read by `parseFrontmatter` with a small subset: scalars (quoted strings, numbers, `true`, `false`, `null`), flow lists (`tags: [a, "b, c"]`) and block lists (`- item` lines). Nested maps and multi-line scalars are skipped with a warning; the body is never affected. `properties` are not inserted by themselves: a wrapping importer merges them into the `meta` of the Typed block it creates, after which the file's own import facts (`filename`, `mimeType`, `size`, `importedAt`) are set and never replaced.

**Subtitles** (`parseSubtitles`): cue blocks are separated by blank lines; identifier lines (the numbers of SubRip), `NOTE`, `STYLE` and `REGION` blocks and cue settings are ignored, inline tags are dropped, cue lines are joined by a space. Times are `hh:mm:ss.mmm`, `mm:ss.mmm` or `hh:mm:ss,mmm` (`cueTime`). A block without a valid time range is skipped and reported.

**BibTeX** (`parseBibtex`): `@type{key, field = {value} | "value" | number | macro # …}`. `author` and `editor` split on `and` outside braces (`Family, Given`, `Given Family` or a braced institution), `year` and `month` (number or English name) or `date` become `issued`, `@string` macros are resolved, `@comment` and `@preamble` are skipped. LaTeX accent commands are not translated. An entry without key or title is skipped with a warning. The block's title is the entry's title; the CSL record carries no title.

## composite-media

`compositeMediaFeature()` contributes the wrapping importer. For every planned file it creates one Typed block of the Block type whose `import` capability takes the file's media, titled with the file name without its extension. The plan's blocks become its body, the plan's `properties` and the file's facts its `meta`. Without a Block type that takes the file, the plan is inserted as it is. So it is when the plan is already Typed blocks and carries no `properties`: a dropped `.vtt` stays the one transcript block and a `.bib` its paper and book blocks, none of them wrapped in a `document` block. Markdown files (frontmatter `properties`) and plain media or text files keep one wrapper per file.

The wrapper needs a plan to wrap; it never reads files itself. It replaces the earlier `wrapImport` pipeline hook, which allowed one contributor: any number of features can now contribute importers, and `wrapImport` no longer exists.

## Duplicates

Before a dropped file is inserted, the Block index is asked whether the workspace holds it already (`backend.index.importCheck`, route `POST /api/editor/import/check`, body `{ sha256?, doi?, title?, type? }`, at least one of the first three). The answer is a list of `ImportMatch` (`address`, `title`, `reason`), at most ten, over the Documents the user may read:

<table>
<thead>
  <tr>
    <th>
      Reason
    </th>
    
    <th>
      A Block matches when
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        sha256
      </code>
    </td>
    
    <td>
      its file has the same SHA-256 (<code>
        properties.sha256
      </code>
      
      : the <code>
        hash
      </code>
      
       of an uploaded media or file block, or of the first body block of a Typed block that wraps one). An authored <code>
        sha256
      </code>
      
       property wins
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        doi
      </code>
    </td>
    
    <td>
      its bibliographic record (<code>
        properties.bib.DOI
      </code>
      
      , else <code>
        properties.doi
      </code>
      
      ) has the same DOI; case, <code>
        doi:
      </code>
      
       and the <code>
        doi.org
      </code>
      
       URL do not count
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      its title has the same words in the same order (<code>
        rankBlockRows
      </code>
      
       ranks, <code>
        titleMatchOf
      </code>
      
       decides); with <code>
        type
      </code>
      
      , the same Block type too
    </td>
  </tr>
</tbody>
</table>

A Block is named once, by the first reason in that order. Of the Blocks of one Document that match for the same reason only the titled ones are kept: a Typed block that wraps an uploaded file and the image inside it carry the same hash, and the Typed block is the one to point at.

The file import (`fileImport`; `useFileImport()`, the `media` feature) asks in two steps. A file that is not text is hashed in the browser first (SHA-256 through WebCrypto), so a file the workspace holds is not uploaded a second time unless the user insists. Otherwise the file is planned and the check asks for every Typed block of the plan (at most 25) with its title, Block type and DOI (`meta.bib.DOI`, else `meta.doi`). A hit is not inserted. A notice offers two answers:

- **Vorhandenen Block referenzieren** inserts a reference chip to the existing Block instead. Of a plan only the duplicate blocks are replaced; a chip into the open Document names no Document.
- **Trotzdem einfügen** inserts the file or the whole plan as it would have been.

The notice stays 30 seconds; closing it skips the file. The drop position is anchored meanwhile, so the answer lands where the file was dropped. A check that fails, a missing `index` port (a backend without Block index) or an editor without reference chips is no duplicate: the file is inserted at once and nothing is asked. The memory backend answers from its own documents, the HTTP backend through the route. `Notice.secondaryAction` is the second button of the notice (Nuxt UI actions, Sonner cancel button).

## Not included

Templates for new Documents are a separate guide (`35.templates`). Frontmatter outside the subset above is not imported.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
