docx

Utilisez cette compétence chaque fois que l'utilisateur souhaite créer, lire, modifier ou manipuler des documents Word (fichiers .docx). Les déclencheurs incluent : toute mention de « Word doc », « word document », « .docx », ou les demandes de production de documents professionnels avec une mise en forme telle que tables des matières, titres, numéros de page ou en-têtes. Utilisez également cette compétence pour extraire ou réorganiser du contenu dans des fichiers .docx, insérer ou remplacer des images dans des documents, effectuer des recherches et remplacements dans des fichiers Word, travailler avec le suivi des modifications ou les commentaires, ou convertir du contenu en un document Word soigné. Si l'utilisateur demande un « rapport », un « mémo », une « lettre », un « modèle » ou un livrable similaire sous forme de fichier Word ou .docx, utilisez cette compétence. N'utilisez PAS cette compétence pour les PDF, les feuilles de calcul, Google Docs, ou les tâches de codage générales sans rapport avec la génération de documents.

npx skills add https://github.com/anthropics/skills --skill docx

Création, édition et analyse de DOCX

Vue d'ensemble

Un fichier .docx est une archive ZIP contenant des fichiers XML.

Référence rapide

Tâche Approche
Lire/analyser le contenu pandoc ou dépacker pour accéder au XML brut
Créer un nouveau document Utiliser docx-js - voir Créer de nouveaux documents ci-dessous
Éditer un document existant Dépacker → éditer le XML → repacker - voir Éditer des documents existants ci-dessous

Convertir .doc en .docx

Les fichiers .doc hérités doivent être convertis avant édition :

python scripts/office/soffice.py --headless --convert-to docx document.doc

Lire le contenu

# Extraction de texte avec suivi des modifications
pandoc --track-changes=all document.docx -o output.md

# Accès au XML brut
python scripts/office/unpack.py document.docx unpacked/

Convertir en images

python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page

Accepter les modifications suivi

Pour générer un document propre avec tous les changements acceptés (nécessite LibreOffice) :

python scripts/accept_changes.py input.docx output.docx

Créer de nouveaux documents

Générer des fichiers .docx avec JavaScript, puis valider. Installation : npm install -g docx

Configuration

const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
        Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
        InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
        PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
        TabStopType, TabStopPosition, Column, SectionType,
        TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
        VerticalAlign, PageNumber, PageBreak } = require('docx');

const doc = new Document({ sections: [{ children: [/* contenu */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));

Validation

Après la création du fichier, validez-le. Si la validation échoue, dépacker, corriger le XML et repacker.

python scripts/office/validate.py doc.docx

Taille de page

// CRITIQUE : docx-js utilise par défaut A4, pas US Letter
// Toujours définir la taille de page explicitement pour des résultats cohérents
sections: [{
  properties: {
    page: {
      size: {
        width: 12240,   // 8,5 pouces en DXA
        height: 15840   // 11 pouces en DXA
      },
      margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // marges de 1 pouce
    }
  },
  children: [/* contenu */]
}]

Tailles de page courantes (unités DXA, 1440 DXA = 1 pouce) :

Papier Largeur Hauteur Largeur contenu (marges 1")
US Letter 12 240 15 840 9 360
A4 (par défaut) 11 906 16 838 9 026

Orientation paysage : docx-js échange largeur/hauteur en interne, donc passez les dimensions portrait et laissez-le gérer l'échange :

size: {
  width: 12240,   // Passez le côté COURT en largeur
  height: 15840,  // Passez le côté LONG en hauteur
  orientation: PageOrientation.LANDSCAPE  // docx-js les échange dans le XML
},
// Largeur contenu = 15840 - marge gauche - marge droite (utilise le long côté)

Styles (Override les titres intégrés)

Utilisez Arial comme police par défaut (universellement supportée). Gardez les titres en noir pour la lisibilité.

const doc = new Document({
  styles: {
    default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt par défaut
    paragraphStyles: [
      // IMPORTANT : Utilisez les IDs exacts pour override les styles intégrés
      { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 32, bold: true, font: "Arial" },
        paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel requis pour TOC
      { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 28, bold: true, font: "Arial" },
        paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
    ]
  },
  sections: [{
    children: [
      new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Titre")] }),
    ]
  }]
});

Listes (N'UTILISEZ JAMAIS de puces unicode)

// ❌ MAUVAIS - ne jamais insérer manuellement de caractères de puce
new Paragraph({ children: [new TextRun("• Élément")] })  // MAUVAIS
new Paragraph({ children: [new TextRun("\u2022 Élément")] })  // MAUVAIS

// ✅ CORRECT - utiliser la config de numérotation avec LevelFormat.BULLET
const doc = new Document({
  numbering: {
    config: [
      { reference: "bullets",
        levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
          style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
      { reference: "numbers",
        levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
          style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
    ]
  },
  sections: [{
    children: [
      new Paragraph({ numbering: { reference: "bullets", level: 0 },
        children: [new TextRun("Élément avec puce")] }),
      new Paragraph({ numbering: { reference: "numbers", level: 0 },
        children: [new TextRun("Élément numéroté")] }),
    ]
  }]
});

// ⚠️ Chaque référence crée une NUMÉROTATION INDÉPENDANTE
// Même référence = continue (1,2,3 puis 4,5,6)
// Référence différente = recommence (1,2,3 puis 1,2,3)

Tableaux

CRITIQUE : Les tableaux ont besoin de largeurs doubles - définir à la fois columnWidths sur le tableau ET width sur chaque cellule. Sans les deux, les tableaux s'affichent mal sur certaines plates-formes.

// CRITIQUE : Toujours définir la largeur du tableau pour un rendu cohérent
// CRITIQUE : Utiliser ShadingType.CLEAR (pas SOLID) pour éviter les fonds noirs
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };

new Table({
  width: { size: 9360, type: WidthType.DXA }, // Toujours utiliser DXA (les pourcentages ne fonctionnent pas dans Google Docs)
  columnWidths: [4680, 4680], // Doivent correspondre à la largeur du tableau (DXA : 1440 = 1 pouce)
  rows: [
    new TableRow({
      children: [
        new TableCell({
          borders,
          width: { size: 4680, type: WidthType.DXA }, // Aussi définir sur chaque cellule
          shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR pas SOLID
          margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Remplissage des cellules (interne, pas ajouté à la largeur)
          children: [new Paragraph({ children: [new TextRun("Cellule")] })]
        })
      ]
    })
  ]
})

Calcul de la largeur du tableau :

Toujours utiliser WidthType.DXAWidthType.PERCENTAGE ne fonctionne pas dans Google Docs.

// Largeur du tableau = somme des columnWidths = largeur du contenu
// US Letter avec marges 1" : 12240 - 2880 = 9360 DXA
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360]  // Doivent correspondre à la largeur du tableau

Règles de largeur :

  • Toujours utiliser WidthType.DXA — jamais WidthType.PERCENTAGE (incompatible avec Google Docs)
  • La largeur du tableau doit égaler la somme de columnWidths
  • La width de la cellule doit correspondre à la columnWidth correspondante
  • Les margins de cellule sont un remplissage interne - ils réduisent la zone de contenu, pas ajoutent à la largeur de la cellule
  • Pour les tableaux pleine largeur : utiliser la largeur du contenu (largeur de page moins marges gauche et droite)

Images

// CRITIQUE : le paramètre type est REQUIS
new Paragraph({
  children: [new ImageRun({
    type: "png", // Requis : png, jpg, jpeg, gif, bmp, svg
    data: fs.readFileSync("image.png"),
    transformation: { width: 200, height: 150 },
    altText: { title: "Titre", description: "Desc", name: "Nom" } // Les trois requis
  })]
})

Sauts de page

// CRITIQUE : PageBreak doit être à l'intérieur d'un Paragraph
new Paragraph({ children: [new PageBreak()] })

// Ou utiliser pageBreakBefore
new Paragraph({ pageBreakBefore: true, children: [new TextRun("Nouvelle page")] })

Hyperliens

// Lien externe
new Paragraph({
  children: [new ExternalHyperlink({
    children: [new TextRun({ text: "Cliquez ici", style: "Hyperlink" })],
    link: "https://example.com",
  })]
})

// Lien interne (signet + référence)
// 1. Créer un signet à la destination
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [
  new Bookmark({ id: "chapter1", children: [new TextRun("Chapitre 1")] }),
]})
// 2. Lien vers lui
new Paragraph({ children: [new InternalHyperlink({
  children: [new TextRun({ text: "Voir Chapitre 1", style: "Hyperlink" })],
  anchor: "chapter1",
})]})

Notes de bas de page

const doc = new Document({
  footnotes: {
    1: { children: [new Paragraph("Source : Rapport annuel 2024")] },
    2: { children: [new Paragraph("Voir appendice pour la méthodologie")] },
  },
  sections: [{
    children: [new Paragraph({
      children: [
        new TextRun("Le revenu a augmenté de 15 %"),
        new FootnoteReferenceRun(1),
        new TextRun(" avec des métriques ajustées"),
        new FootnoteReferenceRun(2),
      ],
    })]
  }]
});

Tabulateurs

// Aligner le texte à droite sur la même ligne (p.ex. date opposée à un titre)
new Paragraph({
  children: [
    new TextRun("Nom de l'entreprise"),
    new TextRun("\tJanvier 2025"),
  ],
  tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
})

// Leader en pointillés (p.ex. style TOC)
new Paragraph({
  children: [
    new TextRun("Introduction"),
    new TextRun({ children: [
      new PositionalTab({
        alignment: PositionalTabAlignment.RIGHT,
        relativeTo: PositionalTabRelativeTo.MARGIN,
        leader: PositionalTabLeader.DOT,
      }),
      "3",
    ]}),
  ],
})

Mises en page multi-colonnes

// Colonnes de largeur égale
sections: [{
  properties: {
    column: {
      count: 2,          // nombre de colonnes
      space: 720,        // écart entre les colonnes en DXA (720 = 0,5 pouce)
      equalWidth: true,
      separate: true,    // ligne verticale entre les colonnes
    },
  },
  children: [/* le contenu s'écoule naturellement sur les colonnes */]
}]

// Colonnes de largeur personnalisée (equalWidth doit être false)
sections: [{
  properties: {
    column: {
      equalWidth: false,
      children: [
        new Column({ width: 5400, space: 720 }),
        new Column({ width: 3240 }),
      ],
    },
  },
  children: [/* contenu */]
}]

Forcer un saut de colonne avec une nouvelle section utilisant type: SectionType.NEXT_COLUMN.

Table des matières

// CRITIQUE : Les titres doivent utiliser HeadingLevel UNIQUEMENT - pas de styles personnalisés
new TableOfContents("Table des matières", { hyperlink: true, headingStyleRange: "1-3" })

En-têtes/Pieds de page

sections: [{
  properties: {
    page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 pouce
  },
  headers: {
    default: new Header({ children: [new Paragraph({ children: [new TextRun("En-tête")] })] })
  },
  footers: {
    default: new Footer({ children: [new Paragraph({
      children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
    })] })
  },
  children: [/* contenu */]
}]

Règles critiques pour docx-js

  • Définir explicitement la taille de page - docx-js utilise par défaut A4 ; utiliser US Letter (12240 x 15840 DXA) pour les documents US
  • Paysage : passer les dimensions portrait - docx-js échange largeur/hauteur en interne ; passer le côté court en width, le côté long en height, et définir orientation: PageOrientation.LANDSCAPE
  • Ne jamais utiliser \n - utiliser des éléments Paragraph séparés
  • Ne jamais utiliser de puces unicode - utiliser LevelFormat.BULLET avec la config de numérotation
  • PageBreak doit être dans Paragraph - standalone crée du XML invalide
  • ImageRun nécessite type - toujours spécifier png/jpg/etc
  • Toujours définir la width du tableau avec DXA - jamais utiliser WidthType.PERCENTAGE (ne fonctionne pas dans Google Docs)
  • Les tableaux ont besoin de largeurs doubles - tableau columnWidths ET cellule width, les deux doivent correspondre
  • Largeur du tableau = somme des columnWidths - pour DXA, s'assurer qu'elles correspondent exactement
  • Toujours ajouter des marges de cellule - utiliser margins: { top: 80, bottom: 80, left: 120, right: 120 } pour un remplissage lisible
  • Utiliser ShadingType.CLEAR - jamais SOLID pour l'ombrage du tableau
  • Ne jamais utiliser les tableaux comme séparateurs/règles - les cellules ont une hauteur minimale et s'affichent comme des cases vides (y compris dans les en-têtes/pieds de page) ; utiliser border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } } sur un Paragraph à la place. Pour les pieds de page à deux colonnes, utiliser les tabulateurs (voir section Tabulateurs), pas les tableaux
  • TOC nécessite HeadingLevel uniquement - pas de styles personnalisés sur les paragraphes de titre
  • Override les styles intégrés - utiliser les IDs exacts : "Heading1", "Heading2", etc.
  • Inclure outlineLevel - requis pour TOC (0 pour H1, 1 pour H2, etc.)

Éditer des documents existants

Suivre les 3 étapes dans l'ordre.

Étape 1 : Dépacker

python scripts/office/unpack.py document.docx unpacked/

Extrait le XML, le formate joliment, fusionne les runs adjacents et convertit les guillemets intelligents en entités XML (“ etc.) pour qu'ils survivent à l'édition. Utiliser --merge-runs false pour ignorer la fusion de runs.

Étape 2 : Éditer le XML

Éditer les fichiers dans unpacked/word/. Voir la section Référence XML ci-dessous pour les motifs.

Utiliser "Claude" comme auteur pour les modifications suivi et commentaires, sauf si l'utilisateur demande explicitement d'utiliser un nom différent.

Utiliser directement l'outil Edit pour remplacer des chaînes. Ne pas écrire de scripts Python. Les scripts introduisent une complexité inutile. L'outil Edit montre exactement ce qui est remplacé.

CRITIQUE : Utiliser des guillemets intelligents pour le nouveau contenu. Lors de l'ajout de texte avec apostrophes ou guillemets, utiliser des entités XML pour produire des guillemets intelligents :

<!-- Utiliser ces entités pour une typographie professionnelle -->
<w:t>Voici&#x2019;s une citation : &#x201C;Bonjour&#x201D;</w:t>
Entité Caractère
&#x2018; ' (simple gauche)
&#x2019; ' (simple droite / apostrophe)
&#x201C; " (double gauche)
&#x201D; " (double droite)

Ajouter des commentaires : Utiliser comment.py pour gérer le boilerplate sur plusieurs fichiers XML (le texte doit être pré-échappé XML) :

python scripts/comment.py unpacked/ 0 "Texte de commentaire avec &amp; et &#x2019;"
python scripts/comment.py unpacked/ 1 "Texte de réponse" --parent 0  # réponse au commentaire 0
python scripts/comment.py unpacked/ 0 "Texte" --author "Auteur personnalisé"  # nom d'auteur personnalisé

Puis ajouter les marqueurs à document.xml (voir Commentaires dans la Référence XML).

Étape 3 : Repacker

python scripts/office/pack.py unpacked/ output.docx --original document.docx

Valide avec réparation automatique, condense le XML et crée le DOCX. Utiliser --validate false pour ignorer.

La réparation automatique corrigera :

  • durableId >= 0x7FFFFFFF (régénère un ID valide)
  • xml:space="preserve" manquant sur <w:t> avec espaces

La réparation automatique ne corrigera pas :

  • XML malformé, imbrication d'éléments invalide, relations manquantes, violations de schéma

Pièges courants

  • Remplacer des éléments <w:r> entiers : Lors de l'ajout de modifications suivi, remplacer le bloc <w:r>...</w:r> entier par <w:del>...<w:ins>... comme frères. Ne pas injecter de balises de suivi de modifications à l'intérieur d'un run.
  • Préserver le formatage <w:rPr> : Copier le bloc <w:rPr> original du run dans vos runs de suivi de modifications pour maintenir le gras, la taille de police, etc.

Référence XML

Conformité au schéma

  • Ordre des éléments dans <w:pPr> : <w:pStyle>, <w:numPr>, <w:spacing>, <w:ind>, <w:jc>, <w:rPr> en dernier
  • Espaces : Ajouter xml:space="preserve" à <w:t> avec espaces en début/fin
  • RSIDs : Doivent être 8 chiffres hex (p.ex. 00AB1234)

Modifications suivi

Insertion :

<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:t>texte inséré</w:t></w:r>
</w:ins>

Suppression :

<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:delText>texte supprimé</w:delText></w:r>
</w:del>

À l'intérieur de <w:del> : Utiliser <w:delText> au lieu de <w:t>, et <w:delInstrText> au lieu de <w:instrText>.

Éditions minimales - marquer uniquement ce qui change :

<!-- Changer "30 jours" en "60 jours" -->
<w:r><w:t>Le délai est </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
  <w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
  <w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> jours.</w:t></w:r>

Suppression de paragraphes/éléments de liste entiers - lors de la suppression de TOUT le contenu d'un paragraphe, marquer aussi le saut de paragraphe comme supprimé pour qu'il fusionne avec le paragraphe suivant. Ajouter <w:del/> à l'intérieur de <w:pPr><w:rPr> :

<w:p>
  <w:pPr>
    <w:numPr>...</w:numPr>  <!-- numérotation de liste si présente -->
    <w:rPr>
      <w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
    </w:rPr>
  </w:pPr>
  <w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
    <w:r><w:delText>Contenu entier du paragraphe en cours de suppression...</w:delText></w:r>
  </w:del>
</w:p>

Sans le <w:del/> dans <w:pPr><w:rPr>, accepter les modifications laisse un paragraphe/élément de liste vide.

Rejeter l'insertion d'un autre auteur - imbriquer la suppression à l'intérieur de son insertion :

<w:ins w:author="Jane" w:id="5">
  <w:del w:author="Claude" w:id="10">
    <w:r><w:delText>texte inséré par eux</w:delText></w:r>
  </w:del>
</w:ins>

Restaurer la suppression d'un autre auteur - ajouter une insertion après (ne pas modifier leur suppression) :

<w:del w:author="Jane" w:id="5">
  <w:r><w:delText>texte supprimé</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
  <w:r><w:t>texte supprimé</w:t></w:r>
</w:ins>

Commentaires

Après avoir exécuté comment.py (voir Étape 2), ajouter les marqueurs à document.xml. Pour les réponses, utiliser le drapeau --parent et imbriquer les marqueurs à l'intérieur du parent.

CRITIQUE : <w:commentRangeStart> et <w:commentRangeEnd> sont des frères de <w:r>, jamais à l'intérieur de <w:r>.

<!-- Les marqueurs de commentaire sont des enfants directs de w:p, jamais à l'intérieur de w:r -->
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:delText>supprimé</w:delText></w:r>
</w:del>
<w:r><w:t> plus de texte</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>

<!-- Commentaire 0 avec réponse 1 imbriquée à l'intérieur -->
<w:commentRangeStart w:id="0"/>
  <w:commentRangeStart w:id="1"/>
  <w:r><w:t>texte</w:t></w:r>
  <w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>

Images

  1. Ajouter le fichier image à word/media/
  2. Ajouter la relation à word/_rels/document.xml.rels :
    <Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
  3. Ajouter le type de contenu à [Content_Types].xml :
    <Default Extension="png" ContentType="image/png"/>
  4. Référencer dans document.xml :
    <w:drawing>
    <wp:inline>
     <wp:extent cx="914400" cy="914400"/>  <!-- EMUs : 914400 = 1 pouce -->
     <a:graphic>
       <a:graphicData uri=".../picture">
         <pic:pic>
           <pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
         </pic:pic>
       </a:graphicData>
     </a:graphic>
    </wp:inline>
    </w:drawing>

Dépendances

  • pandoc : Extraction de texte
  • docx : npm install -g docx (nouveaux documents)
  • LibreOffice : Conversion PDF (auto-configuré pour les environnements en bac à sable via scripts/office/soffice.py)
  • Poppler : pdftoppm pour les images

Skills similaires