регулярное выражение — извлечение аккордов из вкладки с помощью переполнения стека

Я теряю прическу, пытаясь понять, как анализировать музыкальную (текстовую) вкладку, используя preg_match_all и PREG_OFFSET_CAPTURE.

пример вход:

[D#] [G#] [Fm]
[C#] [Fm] [C#] [Fm] [C#] [Fm]

[C]La la la la la la [Fm]la la la la [D#]

[Fm]I made this song Cause I [Bbm]love you
[C]I made this song just for [Fm]you [D#]
[Fm]I made this song deep in [Bbm]my heart

На выходе я пытаюсь получить:

D# G# Fm
C# Fm C# Fm C# Fm

C                 Fm          D#
La la la la la la la la la la

Fm                       Bbm
I made this song Cause I love you

C                     Fm  D#
I made this song just for you

Fm                       Bbm
I made this song deep in my heart

И, наконец, я хочу обернуть аккорды тегами HTML.

Обратите внимание, что промежутки между аккордами должны точно соответствовать положению этих аккордов в исходном вводе.

Я начал анализировать входные данные построчно, обнаруживать аккорды, определять их положение, но мой код не работает …
Там что-то не так в моей функции line_extract_chords, это работает не так, как должно.

Есть идеи ?

<style>
body{
font-family: monospace;
white-space: pre;
</style>

<?php

function parse_song($content){
$lines = explode(PHP_EOL, $content); //explode lines

foreach($lines as $key=>$line){
$chords_line = line_extract_chords($line);
$lines[$key] = implode("\n\r",(array)$chords_line);
}

return implode("\n\r",$lines);
}

function line_extract_chords($line){

$line_chords = null; //text line with chords, used to compute offsets
$line_chords_html = null; //line with chords links
$found_chords = array();

$line = html_entity_decode($line); //remove special characters (would make offset problems)

preg_match_all("/\[([^\]]*)\]/", $line, $matches, PREG_OFFSET_CAPTURE);

$chord_matches = array();

if ( $matches[1] ){
foreach($matches[1] as $key=>$chord_match){

$chord = $chord_match[0];


$position = $chord_match[1];
$offset= $position;
$offset-= 1; //left bracket
$offset-=strlen($line_chords); //already filled line

//previous matches
if ($found_chords){
$offset -= strlen(implode('',$found_chords));
$offset -= 2*(count($found_chords)); //brackets for previous chords
}

$chord_html = '<a href="#">'.$chord.'</a>';

//add spaces
if ($offset>0){
$line_chords.= str_repeat(" ", $offset);
$line_chords_html.= str_repeat(" ", $offset);
}

$line_chords.=$chord;
$line_chords_html.=$chord_html;
$found_chords[] = $chord;

}

}

$line = htmlentities($line); //revert html_entity_decode()

if ($line_chords){
$line = preg_replace('/\[([^\]]*)\]/', '', $line);
return array($line_chords_html,$line);
}else{
return $line;
}

}
?>

4

Решение

Я хотел бы предложить гораздо более простой подход.
Он основан на предположении, что входные данные на самом деле так же универсальны для разбора, как вы описали здесь.

<style>
.line{
font-family: monospace;
white-space: pre;
margin-bottom:0.75rem;
}

.group{
display: inline-block;
margin-right: 0.5rem;
}
.group .top,
.group .top{
display: block;
}
</style>
<?php

$input = "[D#] [G#] [Fm]
[C#] [Fm] [C#] [Fm] [C#] [Fm]

[C]La la la la la la [Fm]la la la la [D#]

[Fm]I made this song Cause I [Bbm]love you
[C]I made this song just for [Fm]you [D#]
[Fm]I made this song deep in [Bbm]my heart";

$output = '';

$inputLines = explode(PHP_EOL,$input);

foreach($inputLines as $line){
$output .='<div class="line">';

if (!strlen($line)){
$output .= '&nbsp;';
}
else{
$inputWords = explode(' ',$line);

foreach($inputWords as $word){
if (preg_match('/^\[(.+)\](.+)$/', $word, $parts)){
$output .='<span class="group"><span class="top">'.$parts[1].'</span><span class="bottom">'.$parts[2].'</span></span>';
}
elseif(preg_match('/^\[(.+)\]$/', $word, $parts)){
$output .='<span class="group"><span class="top">'.$parts[1].'</span><span class="bottom">&nbsp;</span></span>';
}
else{
$output .='<span class="group"><span class="top">&nbsp;</span><span class="bottom">'.$word.'</span></span>';
}
}
}

$output .='</div>';

}
die ($output);

То, что сделано здесь, довольно просто. Сценарий только придает смысл данным аккордов, оборачивая их в HTML. Позиционирование и представление с помощью CSS.

Также это демонстрирует, что у вас есть небольшая ошибка в том, как ваши примерные аккорды транслируются в выходной пример. Fm D# в строке 5, кажется, одно место. По крайней мере, я на это надеюсь.

ДОБАВЛЯТЬ:

Почему ваш код не работает.

Ну, это действительно так. Что не сработало, так это его презентация. Вы посчитали буквы в одной строке и заменили их пробелами в другой. Две вещи, которые не работают здесь, как вы могли ожидать:

  1. в базовом HTML несколько последовательных пробелов сокращаются до одного в представлении brwoser
  2. обычно стандартный шрифт любого браузера не моноширинный. Поэтому нет простого способа заменить символ пробелом такой же ширины.

Так что ты с этим делаешь?

  1. Заменив неразрывный пробел () вместо простого пробела, вы можете убедиться, что все ваши пустые пробелы фактически представлены в представлении браузера. Делать это правильно означает установить white-space: pre; как стиль, поэтому пробелы на самом деле распознаются.
  2. Установить моноширинный шрифт (font-family: monospace;), чтобы убедиться, что ваши замены выстраиваются в очередь.

Вот оно:

<style>
body{
font-family: monospace;
white-space: pre;
</style>

<?php


function parse_song($content){
$lines = explode(PHP_EOL, $content); //explode lines

foreach($lines as $key=>$line){
$chords_line = line_extract_chords($line);
$lines[$key] = implode("\n\r",(array)$chords_line);
}

return implode("\n\r",$lines);
}

function line_extract_chords($line){

$line_chords = null; //text line with chords, used to compute offsets
$line_chords_html = null; //line with chords links
$found_chords = array();

$line = html_entity_decode($line); //remove special characters (would make offset problems)

preg_match_all("/\[([^\]]*)\]/", $line, $matches, PREG_OFFSET_CAPTURE);

$chord_matches = array();

if ( $matches[1] ){
foreach($matches[1] as $key=>$chord_match){

$chord = $chord_match[0];


$position = $chord_match[1];
$offset= $position;
$offset-= 1; //left bracket
$offset-=strlen($line_chords); //already filled line

//previous matches
if ($found_chords){
$offset -= strlen(implode('',$found_chords));
$offset -= 2*(count($found_chords)); //brackets for previous chords
}

$chord_html = '<a href="#">'.$chord.'</a>';

//add spaces
if ($offset>0){
$line_chords.= str_repeat(" ", $offset);
$line_chords_html.= str_repeat(" ", $offset);
}

$line_chords.=$chord;
$line_chords_html.=$chord_html;
$found_chords[] = $chord;

}

}

$line = htmlentities($line); //revert html_entity_decode()

if ($line_chords){
$line = preg_replace('/\[([^\]]*)\]/', '', $line);
return array($line_chords_html,$line);
}else{
return $line;
}

}

$input = "[D#] [G#] [Fm]
[C#] [Fm] [C#] [Fm] [C#] [Fm]

[C]La la la la la la [Fm]la la la la [D#]

[Fm]I made this song Cause I [Bbm]love you
[C]I made this song just for [Fm]you [D#]
[Fm]I made this song deep in [Bbm]my heart";



die(parse_song($input));

Я удалил self:: ссылка, чтобы заставить его работать автономно.

Таким образом, вы на самом деле не написали ничего плохого здесь. Вы только что испортили презентацию своих результатов.

Тем не менее, вы получите бессмысленный, практически не разбираемый (возможно, для интерпретации) фрагмент текста. Этап анализа входных данных должен быть направлен на придание значения данным. Если это было способом HTML или XML-разметки или даже JSON, например, не имеет значения. Но вы должны превратить простой текст в структурированные данные.

Таким образом, вы можете легко оформить его. Вы можете идентифицировать отдельные части всей структуры или отфильтровать их.

3

Другие решения

Хорошо, я наконец нашел способ заставить это работать, основываясь на ответе Стефана, но подправил разбить строку, когда достигнут предел между аккордами и словами.

<style>
.ugs-song{
font-family: monospace;
white-space: pre;
margin-bottom:0.75rem;
}

.ugs-song-line-chunk{
display: inline-block;
}
.ugs-song-line-chunk .top,
.ugs-song-line-chunk .bottom{
display: block;
}
</style>

<?php

function parse_song($content){

$input_lines = explode(PHP_EOL, $content); //explode lines

$chunks_pattern = '~ \h*
(?|        # open a "branch reset group"( \[ [^]]+ ] (?: \h* \[ [^]]+ ] )*+ ) # one or more chords in capture group 1

( [^[]* (?<=) )  # eventual lyrics (group 2)
|                      # OR
()                   # no chords (group 1)
( [^[]* [^[] )   # lyrics (group 2)
)          # close the "branch reset group"~x';

$chords_pattern = '/\[([^]]*)\]/';

//get line chunks
$all_lines_chunks = null;

foreach ((array)$input_lines as $key=>$input_line){
if (preg_match_all($chunks_pattern, $input_line, $matches, PREG_SET_ORDER)) {
$all_lines_chunks[$key] = array_map(function($i) { return [$i[1], $i[2]]; }, $matches);
}
}

foreach ((array)$all_lines_chunks as $key=>$line_chunks){
$line_html = null;

foreach ((array)$line_chunks as $key=>$single_line_chunk){

$chords_html = null;
$words_html = null;

if ($chords_content = $single_line_chunk[0]){

if (preg_match_all($chords_pattern, $chords_content, $matches, PREG_SET_ORDER)) {

$chords_content = null; //reset it

foreach ((array)$matches as $match){
$chord_str = $match[1];
$chords_content.= sprintf('<a class="ugs-song-chord" href="#">%s</a>',$chord_str);



}
}
}

if (!$chords_content) $chords_content = "&nbsp;"; //force content if empty !
$chords_html = sprintf('<span class="top">%s</span>',$chords_content);


if (!$words_content = $single_line_chunk[1]) $words_content = "&nbsp;"; //force content if empty !
$words_content = preg_replace('/\s(?=\S*$)/',"&nbsp;",$words_content); //replace last space by non-breaking space (span would trim a regular space)


$words_html = sprintf('<span class="bottom">%s</span>',$words_content);

$line_html.= sprintf('<div class="ugs-song-chunk">%s</div>',$chords_html.$words_html);
}

$all_lines_html[]=sprintf('<div class="ugs-song-line">%s</div>',$line_html);
}

return implode(PHP_EOL,$all_lines_html);

}

$input = "[C]Hush me, tou[C]ch me
[Gm]Perfume, the wind and the lea[C]ves
[C]Hush me, tou[C]ch me
[Gm]The burns, the holes in the she[C]ets";

echo parse_song($input);
?>
1

По вопросам рекламы [email protected]