From 079b0eb510fdd092740af717b51532780e231737 Mon Sep 17 00:00:00 2001 From: klangme1ster <126453713+klangme1ster@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:04:36 +0200 Subject: [PATCH] Fix (file names): Keep duplicate page postfixes and avoid broken attachment and image links - Duplicate page names: the '-1', '-2' postfix was appended before truncating to mdFileNameAndFolderNameMaxLength, so for long names it was cut off again, and the pages overwrote each other. Duplicates are now detected on the truncated name (which also catches long names that only differ after the cut-off), and the postfix survives truncation (new Truncate-PathFileName -KeepSuffix). - Inserted attachments: references were replaced with a plain search, so with attachments '11-36-00002.pdf' and 'Datenblatt_11-36-00002.pdf' on one page, the first name also matched inside the second, nesting one link inside the other. Only whole file names are matched now. - Images: image file names are prefixed with the page path, which can contain characters like '#' and "'" (e.g. 'c't-expert-community--Newsletter-#10-image1.jpg'). A '#' in a link is read as a URL fragment, breaking the image. Image names now go through Remove-InvalidFileNameCharsInsertedFiles, like inserted attachments. Co-Authored-By: Claude Opus 5.5 --- ConvertOneNote2MarkDown-v2.Tests.ps1 | 72 ++++++++++++++++++++++++++++ ConvertOneNote2MarkDown-v2.ps1 | 36 +++++++++----- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/ConvertOneNote2MarkDown-v2.Tests.ps1 b/ConvertOneNote2MarkDown-v2.Tests.ps1 index 72d1ae7..8645573 100644 --- a/ConvertOneNote2MarkDown-v2.Tests.ps1 +++ b/ConvertOneNote2MarkDown-v2.Tests.ps1 @@ -313,6 +313,24 @@ Describe "Truncate-PathFileName" -Tag 'Unit' { $path | Truncate-PathFileName -Length $length | Should -Be $expectedPath } + It "Should keep a given -KeepSuffix when truncating" { + $drive = "C:$( [io.path]::DirectorySeparatorChar )" # E.g. C:\ + $path = $drive + ("a" * 1000) + '-1' + $length = 123 + $expectedPath = $drive + ("a" * 121) + '-1' + + $path | Truncate-PathFileName -Length $length -KeepSuffix '-1' | Should -Be $expectedPath + } + + It "Should ignore -KeepSuffix if the filename does not end with it" { + $drive = "C:$( [io.path]::DirectorySeparatorChar )" # E.g. C:\ + $path = $drive + ("a" * 1000) + $length = 123 + $expectedPath = $drive + ("a" * 123) + + $path | Truncate-PathFileName -Length $length -KeepSuffix '-1' | Should -Be $expectedPath + } + } } @@ -833,6 +851,32 @@ Describe 'New-SectionGroupConversionConfig' -Tag 'Unit' { } } + It "Should not match an attachment's name inside a longer attachment name" { + Mock Get-OneNotePageContent { + @' + + + + + + + + + +'@ -as [xml] + } + + $pageCfg = @( New-SectionGroupConversionConfig @params 6>$null ) | Select-Object -First 1 + $content = '\<\\> and \<\<11-36-00002.pdf\>\>' + foreach ($m in ($pageCfg['mutations'] | Where-Object { $_['description'] -eq 'Change inserted attachment(s) filename references' })) { + foreach ($r in $m['replacements']) { + $content = $content -replace $r['searchRegex'], $r['replacement'] + } + } + + $content | Should -Be "\<\<[Datenblatt_11-36-00002.pdf]($( $pageCfg['mediaPathPandoc'] )/Datenblatt_11-36-00002.pdf)\>\> and \<\<[11-36-00002.pdf]($( $pageCfg['mediaPathPandoc'] )/11-36-00002.pdf)\>\>" + } + It "Should generate fully unique docx file for each page, even for identically-named pages in a section" { $result = @( New-SectionGroupConversionConfig @params 6>$null ) @@ -885,6 +929,34 @@ Describe 'New-SectionGroupConversionConfig' -Tag 'Unit' { } } + It "Should keep the unique postfix of identically named pages when truncating long names" { + $longName = 'Some long name ' * 20 + $fakeHierarchy = @" + + + + + + + + + + +"@ -as [xml] + $params['SectionGroups'] = $fakeHierarchy.Notebooks.Notebook + $params['Config']['mdFileNameAndFolderNameMaxLength']['value'] = 100 + + $result = @( New-SectionGroupConversionConfig @params 6>$null ) + + $result.Count | Should -Be 3 + $result[1]['fileName'] | Should -Match '-1\.md$' + $result[2]['fileName'] | Should -Match '-2\.md$' + @( $result | ForEach-Object { $_['filePathNormal'] } | Select-Object -Unique ).Count | Should -Be 3 + foreach ($pageCfg in $result) { + $pageCfg['fileName'].Length | Should -BeLessOrEqual 100 + } + } + It "Should honor config notesdestpath" { if ($env:OS -match 'windows') { $params['Config']['notesdestpath']['value'] = 'D:\foo\bar' diff --git a/ConvertOneNote2MarkDown-v2.ps1 b/ConvertOneNote2MarkDown-v2.ps1 index 3668cba..bb5a62e 100644 --- a/ConvertOneNote2MarkDown-v2.ps1 +++ b/ConvertOneNote2MarkDown-v2.ps1 @@ -386,6 +386,11 @@ Function Truncate-PathFileName { [ValidateRange(0,255)] [int] $Length + , + # A suffix (e.g. the '-1' postfix of a duplicate page name) that must survive truncation + [Parameter()] + [string] + $KeepSuffix = '' ) process { @@ -404,7 +409,11 @@ Function Truncate-PathFileName { $name = Split-Path $Path -Leaf if ($name.Length -gt $maxLength) { $parent = Split-Path $Path -Parent - $truncatedName = $name.Substring(0, $maxLength) + $truncatedName = if ($KeepSuffix -and $name.EndsWith($KeepSuffix) -and $maxLength -gt $KeepSuffix.Length) { + $name.Substring(0, $maxLength - $KeepSuffix.Length) + $KeepSuffix + }else { + $name.Substring(0, $maxLength) + } [io.path]::combine( $parent, $truncatedName ) }else { $Path @@ -800,17 +809,18 @@ Function New-SectionGroupConversionConfig { $pageCfg['filePathRel'] = & { $filePathRel = "$( $pageCfg['pagePrefix'] )$( $pageCfg['nameCompat'] )" - # in case multiple pages with the same name exist in a section, postfix the filename + # In case multiple pages with the same name exist in a section, postfix the filename. + # Compare the names as they will be written (truncated to fit '.md'), so two long names that only differ + # after the cut-off are also recognized as duplicates. The postfix is kept when truncating below, so it can't be cut off again. + $pageCfg['filePathRelKey'] = $filePathRel | Truncate-PathFileName -Length ($config['mdFileNameAndFolderNameMaxLength']['value'] - 3) $recurrence = 0 foreach ($p in $sectionCfg['pages']) { - if ($p['pagePrefix'] -eq $pageCfg['pagePrefix'] -and $p['pathFromRoot'] -eq $pageCfg['pathFromRoot']) { + if ($p['filePathRelKey'] -eq $pageCfg['filePathRelKey']) { $recurrence++ } } - if ($recurrence -gt 0) { - $filePathRel = "$filePathRel-$recurrence" - } - $filePathRel | Truncate-PathFileName -Length $config['mdFileNameAndFolderNameMaxLength']['value'] # Truncate to no more than 255 characters so we don't hit the folder name limit on most file systems on Windows / Linux + $pageCfg['filePathRelPostfix'] = if ($recurrence -gt 0) { "-$recurrence" } else { '' } + "$filePathRel$( $pageCfg['filePathRelPostfix'] )" | Truncate-PathFileName -Length $config['mdFileNameAndFolderNameMaxLength']['value'] -KeepSuffix $pageCfg['filePathRelPostfix'] # Truncate to no more than 255 characters so we don't hit the folder name limit on most file systems on Windows / Linux } $pageCfg['filePathRelUnderscore'] = $pageCfg['filePathRel'].Replace( [io.path]::DirectorySeparatorChar, '_' ) $pageCfg['filePathNormal'] = & { @@ -819,7 +829,7 @@ Function New-SectionGroupConversionConfig { }else { [io.path]::combine( $cfg['notesDirectory'], $sectionCfg['nameCompat'], "$( $pageCfg['filePathRel'] )" ) } - "$( $pathWithoutExtension | Truncate-PathFileName -Length ($config['mdFileNameAndFolderNameMaxLength']['value'] - 3) ).md" # Truncate to no more than 255 characters so we don't hit the file name limit on Windows / Linux + "$( $pathWithoutExtension | Truncate-PathFileName -Length ($config['mdFileNameAndFolderNameMaxLength']['value'] - 3) -KeepSuffix $pageCfg['filePathRelPostfix'] ).md" # Truncate to no more than 255 characters so we don't hit the file name limit on Windows / Linux } $pageCfg['filePathLong'] = "\\?\$( $pageCfg['filePathNormal'] )" # A non-Win32 path. Prefixing with '\\?\' allows Windows Powershell <= 5 (based on Win32) to support long absolute paths. $pageCfg['filePath'] = if ($PSVersionTable.PSVersion.Major -le 5) { @@ -896,7 +906,9 @@ Function New-SectionGroupConversionConfig { description = 'Change inserted attachment(s) filename references' replacements = @( @{ - searchRegex = [regex]::Escape( $attachmentCfg['object'].preferredName ) + # Only where the name is not part of a longer file name: with attachments '11-36-00002.pdf' and 'Datenblatt_11-36-00002.pdf' + # on one page, a plain search for the first one also matched inside the second, nesting one link inside the other + searchRegex = '(?