ResourceSorter.php

<?php

namespace Lia\Addon;


/**
 * Convenience class for sorting css & js files before they are concatenated together.
 */
class ResourceSorter extends \Lia\Addon {

    public string $fqn = 'lia:server.resourcesorter';


    protected array $prepend = ['css'=>[], 'js'=>[]];
    protected array $postpend=['css'=>[], 'js'=>[]];


    /**
     * Give an array of file names in the order they should be sent to the browser.
     *
     * @param $ext the file extension js or css
     * @param $names an array of relative file paths, in the order you want them. There MUST NOT be leading `/`. There MAY be as many `/` as you like
     * @param $prepend_all TRUE to put these BEFORE unsorted files. FALSE to put them AFTER unsorted files
     * @param $prepend_sorted TRUE to put these BEFORE other sorted files. FALSE to put them AFTER other sorted files.
     */
    public function setResourceOrder(string $ext, array $names, bool $prepend_all=true, bool $prepend_sorted = true){
        $ext = strtolower($ext);
        \Lia\Addon\Resources::from($this->lia)->setSorter($ext, [$this, 'getSortedFiles']);

        $target = &$this->prepend;
        if (!$prepend_all)$target = &$this->postpend;

        foreach ($names as $name){
            if (isset($target[$ext][$name])){
                unset($target[$ext][$name]);
            }
        }
        $new_names = array_combine($names,$names);
        if ($prepend_sorted){
            $target[$ext] =
                array_merge(
                    $new_names,
                    $target[$ext]
                );
        } else {
            $target[$ext] =
                array_merge(
                    $target[$ext],
                    $new_names
                );
        }
    }


    public function getSortedFiles(string $ext, array $unsorted_list): array {

        //@TODO add support for file paths using backslash separators like C:\Whatever\Folder\File.js
        if (count($unsorted_list)==0)return $unsorted_list;
        // get file extension
        $ext = strtolower($ext);
        $prepend_list = $this->prepend[$ext];
        $list_with_prepend = 
            $this->get_sorted_files(
                $unsorted_list, 
                $this->prepend[$ext],
                true
            );

        $final_list = 
            $this->get_sorted_files(
                $list_with_prepend, 
                $this->postpend[$ext],
                false 
            );

        return $final_list;
    }

    /**
     * @param $full_list full list of files to sort
     * @param $sort_list an ordered list of files
     * @param $prepend whether the `$sort_list` files should go at the beginning or end of `$full_list`
     */
    protected function get_sorted_files(array $full_list, array $sort_list, bool $prepend): array {

        if (count($sort_list) == 0)return $full_list;
        // for each resource file,
        // check the entire 'sort' list for any matches
        // One absolute file path might match multiple entries
        // For Example: '/abs/path/theme.css' would match BOTH 'theme.css' AND 'path/theme.css'
        // So the  'last_match_len' check makes sure that the longest-matching relative path take precedence over a shorter path.
        // So if `theme.css` comes BEFORE `path/theme.css` then the actual file `/abs/path/theme.css` will end up going AFTER other files named 'theme.css'.

        $sorted_files = [];
        foreach ($full_list as $full_list_key => $absolute_path_dirty){
            $absolute_path = str_replace(['///','//'], '/', $absolute_path_dirty);
            $last_match_len = 0;
            $sort_index = 0;
            foreach ($sort_list as $rel_path){
                $len = strlen($rel_path);
                if (substr($absolute_path, -$len) == $rel_path){
                    if (!isset($sorted_files[$absolute_path])
                        || $len > $last_match_len){

                        unset($full_list[$full_list_key]);
                        $sorted_files[$absolute_path] = $sort_index;
                        $last_match_len = $len;
                    }
                }
                $sort_index++;
            }
        }
        asort($sorted_files, SORT_NUMERIC);

        $sorted_files = array_keys($sorted_files);

        if ($prepend){
            return array_merge(
                $sorted_files,
                $full_list
            );
        } else {
            return array_merge(
                $full_list,
                $sorted_files
            );
        }

    }

}