Skip to content

Commit 7443520

Browse files
authored
Massage MusicXML v2 (#770)
* wip * wip * refinements * bugfix * refactored for single logging/processing path * staff numbering seems to work, but logging issues remain * working pretty well now * refinements * refactor UI to use modeless dialog * comments * more accurate message * refinements * bugfixes and review suggestions
1 parent 974d7c7 commit 7443520

File tree

4 files changed

+599
-121
lines changed

4 files changed

+599
-121
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mobdebug.lua
3838

3939
src/**/personal*
4040
dist/**/personal*
41+
personal/*
4142

4243
.vscode/settings.json
4344
*/**/.vscode

src/document_options_to_musescore.lua

Lines changed: 26 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ function plugindef()
44
finaleplugin.NoStore = true
55
finaleplugin.Author = "Robert Patterson"
66
finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
7-
finaleplugin.Version = "1.0"
8-
finaleplugin.Date = "October 2, 2024"
7+
finaleplugin.Version = "1.0.1"
8+
finaleplugin.Date = "October 6, 2024"
99
finaleplugin.CategoryTags = "Document"
1010
finaleplugin.MinJWLuaVersion = 0.75
1111
finaleplugin.Notes = [[
@@ -54,21 +54,21 @@ end
5454

5555
-- luacheck: ignore 11./global_dialog
5656

57-
local lfs = require("lfs")
5857
local text = require("luaosutils").text
5958

6059
local mixin = require("library.mixin")
6160
local enigma_string = require("library.enigma_string")
61+
local utils = require("library.utils")
6262

6363
do_folder = do_folder or false
6464

65-
local logfile_name = "FinaleMuseScoreSettingsExportLog.txt"
65+
local LOGFILE_NAME <const> = "FinaleMuseScoreSettingsExportLog.txt"
6666

67-
local musx_extension = ".musx"
68-
local mus_extension = ".mus"
69-
local text_extension = ".mss"
70-
local part_extension = ".part" .. text_extension
71-
local mss_version = "4.40"
67+
local MUSX_EXTENSION <const> = ".musx"
68+
local MUS_EXTENSION <const> = ".mus"
69+
local TEXT_EXTENSION <const> = ".mss"
70+
local PART_EXTENSION <const> = ".part" .. TEXT_EXTENSION
71+
local MSS_VERSION <const> = "4.40"
7272

7373
-- hard-coded scaling values
7474
local EVPU_PER_INCH <const> = 288
@@ -159,22 +159,6 @@ function open_current_prefs()
159159
text_exps:LoadAll()
160160
end
161161

162-
-- returns Lua strings for path, file name without extension, full file path
163-
function get_file_path_no_extension(file_path_str)
164-
local path_name = finale.FCString()
165-
local file_name = finale.FCString()
166-
local file_path = finale.FCString(file_path_str)
167-
file_path:SplitToPathAndFile(path_name, file_name)
168-
local full_file_name = file_name.LuaString
169-
local extension = finale.FCString(file_name.LuaString)
170-
extension:ExtractFileExtension()
171-
if extension.Length > 0 then
172-
file_name:TruncateAt(file_name:FindLast("." .. extension.LuaString))
173-
end
174-
path_name:AssureEndingPathDelimiter()
175-
return path_name.LuaString, file_name.LuaString, full_file_name
176-
end
177-
178162
function set_element_text(style_element, name, value)
179163
local setter_func = "SetText"
180164
if type(value) == "nil" then
@@ -628,7 +612,7 @@ function write_xml(output_path)
628612
local mssxml <close> = tinyxml2.XMLDocument()
629613
mssxml:InsertEndChild(mssxml:NewDeclaration(nil))
630614
local ms_element = mssxml:NewElement("museScore")
631-
ms_element:SetAttribute("version", mss_version)
615+
ms_element:SetAttribute("version", MSS_VERSION)
632616
mssxml:InsertEndChild(ms_element)
633617
local style_element = ms_element:InsertNewChildElement("Style")
634618
currently_processing = output_path
@@ -664,13 +648,13 @@ function process_document(document_file_path)
664648
local parts = finale.FCParts()
665649
parts:LoadAll()
666650
-- it is not actually necessary to switch to the part to get its settings
667-
local path_name, file_name_no_ext = get_file_path_no_extension(document_file_path)
651+
local path_name, file_name_no_ext = utils.split_file_path(document_file_path)
668652
current_is_part = false
669-
write_xml(path_name .. file_name_no_ext .. text_extension)
653+
write_xml(path_name .. file_name_no_ext .. TEXT_EXTENSION)
670654
for part in each(parts) do
671655
if not part:IsScore() then
672656
current_is_part = true
673-
write_xml(path_name .. file_name_no_ext .. part_extension)
657+
write_xml(path_name .. file_name_no_ext .. PART_EXTENSION)
674658
break
675659
end
676660
end
@@ -724,9 +708,10 @@ function create_status_dialog(selected_directory, files_to_process)
724708
dialog:RegisterHandleTimer(function(self, timer)
725709
assert(timer == TIMER_ID, "incorrect timer id value " .. timer)
726710
if #files_to_process <= 0 then
727-
self:GetControl("folder"):SetText(selected_directory)
728-
self:GetControl("file_path"):SetText("Export complete.")
729711
self:StopTimer(TIMER_ID)
712+
self:GetControl("folder"):SetText(selected_directory)
713+
self:GetControl("file_path_label"):SetText("Log:")
714+
self:GetControl("file_path"):SetText(LOGFILE_NAME .. " (export complete)")
730715
currently_processing = selected_directory
731716
log_message("processing complete")
732717
self:GetControl("cancel"):SetText("Close")
@@ -754,40 +739,15 @@ function create_status_dialog(selected_directory, files_to_process)
754739
dialog:RunModeless()
755740
end
756741

757-
function collect_files(utf8_folder_path, files_to_process)
758-
local folder_path_fcstr = finale.FCString(utf8_folder_path)
759-
folder_path_fcstr:AssureEndingPathDelimiter()
760-
utf8_folder_path = folder_path_fcstr.LuaString
761-
local lfs_folder_path = text.convert_encoding(utf8_folder_path, text.get_utf8_codepage(), text.get_default_codepage())
762-
for lfs_finale_doc in lfs.dir(lfs_folder_path) do
763-
if lfs_finale_doc ~= "." and lfs_finale_doc ~= ".." then
764-
local utf8_finale_doc = text.convert_encoding(lfs_finale_doc, text.get_default_codepage(), text.get_utf8_codepage())
765-
if (lfs_finale_doc:sub(-musx_extension:len()) == musx_extension) or (lfs_finale_doc:sub(-mus_extension:len()) == mus_extension) then
766-
if lfs_finale_doc:sub(1, 2) == "._" then
767-
currently_processing = utf8_folder_path .. utf8_finale_doc
768-
log_message("skipping macOS resource fork", true)
769-
else
770-
table.insert(files_to_process, {name = utf8_finale_doc, folder = utf8_folder_path})
771-
end
772-
else
773-
local attr = lfs.attributes(lfs_folder_path .. lfs_finale_doc)
774-
if attr and attr.mode == "directory" then
775-
collect_files(utf8_folder_path .. utf8_finale_doc, files_to_process)
776-
end
777-
end
778-
end
779-
end
780-
end
781-
782742
function select_target(file_path_str)
783-
local path_name, file_name_no_ext = get_file_path_no_extension(file_path_str)
784-
local file_name = file_name_no_ext .. (current_is_part and part_extension or text_extension)
743+
local path_name, file_name_no_ext = utils.split_file_path(file_path_str)
744+
local file_name = file_name_no_ext .. (current_is_part and PART_EXTENSION or TEXT_EXTENSION)
785745
local save_dialog = finale.FCFileSaveAsDialog(finenv.UI())
786746
save_dialog:SetWindowTitle(finale.FCString("Save MuseScore style settings as"))
787-
save_dialog:AddFilter(finale.FCString("*" .. text_extension), finale.FCString("MuseScore Style Settings File"))
747+
save_dialog:AddFilter(finale.FCString("*" .. TEXT_EXTENSION), finale.FCString("MuseScore Style Settings File"))
788748
save_dialog:SetInitFolder(finale.FCString(path_name))
789749
save_dialog:SetFileName(finale.FCString(file_name))
790-
save_dialog:AssureFileExtension(text_extension)
750+
save_dialog:AssureFileExtension(TEXT_EXTENSION)
791751
if not save_dialog:Execute() then
792752
return nil
793753
end
@@ -823,14 +783,18 @@ function document_options_to_musescore()
823783
end
824784
local selected_directory = select_directory()
825785
if selected_directory then
826-
logfile_path = text.convert_encoding(selected_directory, text.get_utf8_codepage(), text.get_default_codepage()) .. logfile_name
786+
logfile_path = text.convert_encoding(selected_directory, text.get_utf8_codepage(), text.get_default_codepage()) .. LOGFILE_NAME
827787
local file <close> = io.open(logfile_path, "w")
828788
if not file then
829789
error("unable to create logfile " .. logfile_path)
830790
end
831791
file:close()
832792
local files_to_process = {}
833-
collect_files(selected_directory, files_to_process)
793+
for folder, filename in utils.eachfile(selected_directory, true) do
794+
if (filename:sub(-MUSX_EXTENSION:len()) == MUSX_EXTENSION) or (filename:sub(-MUS_EXTENSION:len()) == MUS_EXTENSION) then
795+
table.insert(files_to_process, {name = filename, folder = folder})
796+
end
797+
end
834798
create_status_dialog(selected_directory, files_to_process)
835799
end
836800
else

src/library/utils.lua

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,91 @@ function utils.win_mac(windows_value, mac_value)
467467
return mac_value
468468
end
469469

470+
--[[
471+
% split_file_path
472+
473+
Splits a file path into folder, file name, and extension.
474+
475+
@ full_path (string) The full file path in a Lua string.
476+
: (string) the folder path always including the final delimeter slash (macOS) or backslash (Windows). This may be an empty string.
477+
: (string) the filename without its extension
478+
: (string) the extension including its leading "." or an empty string if no extension.
479+
]]
480+
function utils.split_file_path(full_path)
481+
local path_name = finale.FCString()
482+
local file_name = finale.FCString()
483+
local file_path = finale.FCString(full_path)
484+
-- work around bug in SplitToPathAndFile when path is not specified
485+
if file_path:FindFirst("/") >= 0 or (finenv.UI():IsOnWindows() and file_path:FindFirst("\\") >= 0) then
486+
file_path:SplitToPathAndFile(path_name, file_name)
487+
else
488+
file_name.LuaString = full_path
489+
end
490+
-- do not use FCString.ExtractFileExtension() because it has a hard-coded limit of 7 characters (!)
491+
local extension = file_name.LuaString:match("^.+(%..+)$")
492+
extension = extension or ""
493+
if #extension > 0 then
494+
-- FCString.FindLast is unsafe if extension is not ASCII, so avoid using it
495+
local truncate_pos = file_name.Length - finale.FCString(extension).Length
496+
if truncate_pos > 0 then
497+
file_name:TruncateAt(truncate_pos)
498+
else
499+
extension = ""
500+
end
501+
end
502+
path_name:AssureEndingPathDelimiter()
503+
return path_name.LuaString, file_name.LuaString, extension
504+
end
505+
506+
--[[
507+
% eachfile
508+
509+
Iterates a file path using lfs and feeds each directory and file name to a function.
510+
The directory names fed to the iterator function always contain path delimeters at the end.
511+
The following are skipped.
512+
513+
- "." and ".."
514+
- any file name starting withn "._" (These are macOS resource forks and can be seen on Windows as well when searching a macOS shared drive.)
515+
516+
Generates a runtime error for plugin versions before RGP Lua 0.68.
517+
518+
@ directory_path (string) the directory path to search, encoded utf8.
519+
@ [recursive)] (boolean) true if subdirectories should always be searched. Defaults to false.
520+
: (function) iterator function to be used in for loop.
521+
]]
522+
function utils.eachfile(directory_path, recursive)
523+
if finenv.MajorVersion <= 0 and finenv.MinorVersion < 68 then
524+
error("utils.eachfile requires at least RGP Lua v0.68.", 2)
525+
end
526+
527+
recursive = recursive or false
528+
529+
local lfs = require('lfs')
530+
local text = require('luaosutils').text
531+
532+
local fcstr = finale.FCString(directory_path)
533+
fcstr:AssureEndingPathDelimiter()
534+
directory_path = fcstr.LuaString
535+
536+
local lfs_directory_path = text.convert_encoding(directory_path, text.get_utf8_codepage(), text.get_default_codepage())
537+
538+
return coroutine.wrap(function()
539+
for lfs_file in lfs.dir(lfs_directory_path) do
540+
if lfs_file ~= "." and lfs_file ~= ".." then
541+
local utf8_file = text.convert_encoding(lfs_file, text.get_default_codepage(), text.get_utf8_codepage())
542+
local mode = lfs.attributes(lfs_directory_path .. lfs_file, "mode")
543+
if mode == "directory" then
544+
if recursive then
545+
for subdir, subfile in utils.eachfile(directory_path .. utf8_file, recursive) do
546+
coroutine.yield(subdir, subfile)
547+
end
548+
end
549+
elseif (mode == "file" or mode == "link") and lfs_file:sub(1, 2) ~= "._" then -- skip macOS resource files
550+
coroutine.yield(directory_path, utf8_file)
551+
end
552+
end
553+
end
554+
end)
555+
end
556+
470557
return utils

0 commit comments

Comments
 (0)