Jump to content

Separate textarea content in new lines and check if line contains a string


MilenKo
 Share

Recommended Posts

Hello all.

I am working on a Cooking Recipes profile and have a field recipe_nutrition where I add the nutritional values in a textarea field like:

Calories: 2000kj
Fat: 10gr
Carbohydrate: 10gr
Protein: 50gr
Vitamin C: 10mg
Iron: 20mg

So I am using the explode function to separate the lines and it works fine (if I am just styling the values as text list), but now I would like to take the text content before and after the colon and add them in a table. Also I need to find first the line that contains Calories and print the value in frontend. As far as the lines are not added in a specific order, the Calories: 2000kj could be first, second, third line etc. 

In the perfect scenario the line should be checked for a specifict text (in my case Calories) and if it contains it to get the text after the : sign. 

Maybe there is a better approach to using API and modules to achieve a simple Nutrition table with all the values without creating a field for every value but grabbing it from a text content or else? Check the image for better understanding of the goal.

Nutrition-block.jpg

Link to comment
Share on other sites

You can use regex to split lines into parts (name, amount, unit) and use a combination of WireArray and WireData objects. 

In the end you'll have turned a list like this

Proteins: 45.3 g
Calories: 650 kcal
Fat: 30g
Carbs: 1.2g
Sugar: 0.5g
Salt: 1.1g
Saturates: 15g
Fibre: 0.2g

Into one that looks like 

chrome_2017-04-29_07-19-46.thumb.png.791a83565ba6cf0489d8cbc80115d0f8.png

 

$text = $sanitizer->textarea($page->body); // remove markup from body

$nutrition = new WireArray();

// regex pattern to extract values
$pattern =
    '/^' . // beginning of each line
    '\s*' . // may start with some whitespace
    '(?P<name>[^:]+)' . // name (first part until colon)
    '\s*:\s*' . // a colon that may be surrounded by whitespaces
    '(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
    '\s*' .  // after the number there may be some whitespace
    '(?P<unit>\w+)'. // unit (one or more letters)
    '\s*$/'; // there might be some whitespace before the end

foreach (explode("\n", $text) as $line) {
        preg_match($pattern, $line, $parts);
      
        $nutrition->add((new WireData())->setArray([
            'name' => $parts['name'],
            'amount' => floatval($parts['amount']),
            'unit' => $parts['unit']
        ]));
}

// Since $nutrition is a WireArray, you can do things like
// $nutrition->sort('-amount'); // sort by amount descending
// $cal = $nutrition->get('name=calorie');

// then create a list
$content .= "<ul class='nutrition-list'>";
$content .= $nutrition->each('<li class="nutrition"><span class="nutrition__name">{name}</span>
                             <span class="nutrition__value">{amount}</span> <span class="nutrition__unit">{unit}</span></li>');
$content .= "</ul>";


// Keep CSS out of PHP, in its own file
$content .= "<style>
    .nutrition-list {
        list-style: none;
        display: flex;
        flex-wrap: wrap;
    }
    .nutrition {
        flex: 1 0 25%;
    }
    
    .nutrition__name {
        display: block;
        font-weight: bold;
    }
    .nutrition__value {
        font-size: 2em;
    }
    .nutrition__unit {
        opacity: 0.75;
    }
</style>";

 

  • Like 4
Link to comment
Share on other sites

@abdus Thank you very much for the suggestion. It looks like a nice way to do it, however I would need to extract from the textarea field the calories and put them in a different spot too (besides the attached image I have another spot for the calories only).

@Pixrael You have reminded me that I should get more and more familiar with the PW Mods as the two suggestions would be almost a perfect hit. So I will give it a try and see what would be the best approach. The "issue" I see is that different users would have their different order so I should grab the calories vaue to put in the specific place on the frontend and then post the rest in a table as shown on the image. But I feel much closer with your suggestions guys, so your advises were HIGHLY APPRECIATED!

Once I have the code in place, I will share it to allow others to reuse it one day if a need be.

Link to comment
Share on other sites

11 minutes ago, MilenKo said:

I would need to extract from the textarea field the calories and put them in a different spot too

Since $nutrition is a WireArray, you can get calories using selectors and echo it

$cal = $nutrition->get('name%=cal');

echo $cal->name; // "Calorie"
echo $cal->amount; // "650.2"
echo $cal->unit; // "kcal"

anywhere you like.

Check out WireArray documentation

https://processwire.com/api/ref/wire-array/

  • Like 1
Link to comment
Share on other sites

@abdus Sorry, I got too quick to reply before reading the script properly. I guess it is the late hours work (or should I say early hours in here). I think your suggestion would work out of the box without the need of a plugin and as far as I might not need another instance of textarea to filter like the nutrition, I would start with it first. Basically what you provided is almost the complete sollution where I would only need to adjust the field names etc.

Extreme grattitude!

P.S. I will read about the Wire-array as it seems to be pretty powerful and useful.

Link to comment
Share on other sites

OK, I tested @abdus method and it worked like a charm. It takes the nutrition name, value and measure and puts them to the styling super easy. One thing I am thinking to improve is to put the items in a specific order but not as they are typed in the list. 

So getting back to the previous nutrition list

Proteins: 45.3 g
Calories: 650 kcal
Fat: 30g
Carbs: 1.2g
Sugar: 0.5g
Salt: 1.1g
Saturates: 15g
Fibre: 0.2g

The usual presentation of nutrition goes in this order:

Calories: 650 kcal
Fat: 30 gr.
Saturated: 15 gr.
Salt: 1.1 gr.

Carbs: 1.2 gr.
Fibre: 0.2 gr.
Sugar: 0.5 gr
Proteins: 45.3 gr.

If it were only these values, I could extract them using the $cal = $nutrition->get('name%=cal') approach, but the list might go big if I add vitamins, minerals etc. So is there a way to order the list in the array?

Link to comment
Share on other sites

18 minutes ago, MilenKo said:

So is there a way to order the list in the array

// $nutrition = new WireArray();

$order = [
    'Calories',
    'Fat',
    'Saturated',
    'Salt',
    'Carbs',
    'Fibre',
    'Sugar',
    'Proteins',
];

$markup = '';
foreach ($order as $o) {
    $item = $nutrition->get("name%=$o");

    // render item
    $markup .= $item->name;
    $markup .= $item->amount;
    $markup .= $item->unit;
}

// use markup
echo $markup;

A more flexible way would be to create a Page field with AsmSelect that you can add/remove and reorder items. But for a simple setup, this should suffice.

  • Like 1
Link to comment
Share on other sites

@abdus I tried to implement your order code but it got messed up and repeats every field 3 times or more. Here is the code I used:

Spoiler

<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
$nutrition = new WireArray();

// regex pattern to extract values
$pattern =
    '/^' . // beginning of each line
    '\s*' . // may start with some whitespace
    '(?P<name>[^:]+)' . // name (first part until colon)
    '\s*:\s*' . // a colon that may be surrounded by whitespaces
    '(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
    '\s*' .  // after the number there may be some whitespace
    '(?P<unit>\w+)'. // unit (one or more letters)
    '\s*$/'; // there might be some whitespace before the end
	
	foreach (explode("\n", $text) as $line) {
        preg_match($pattern, $line, $parts);
      
        $nutrition->add((new WireData())->setArray([
            'name' => $parts['name'],
            'amount' => floatval($parts['amount']),
            'unit' => $parts['unit']
        ]));
	}
	
	$order = [
		'Calories',
		'Fat',
		'Saturated',
		'Salt',
		'Carbs',
		'Fibre',
		'Sugar',
		'Proteins',
	];

	$markup = '';
	foreach ($order as $o) {
		$item = $nutrition->get("name%=$o");	
	

// Since $nutrition is a WireArray, you can do things like
// $nutrition->sort('-amount'); // sort by amount descending
// $cal = $nutrition->get('name=calorie');

// then create a list


$content .= "<ul>";
$content .= "<li><div class='cs-nutrition-item-description'>$item->name</div><div class='cs-nutrition-item-value'>$item->amount $item->unit</div></li>";
$content .= "</ul>";
echo $content;
}
?>

 

 

Link to comment
Share on other sites

2 minutes ago, MilenKo said:

echo $content;
}
?>

 

You're outputting $content with each iteration. Try putting echo $content after the closing brace } of foreach() {}

foreach ($order as $o) {
    $item = $nutrition->get("name%=$o");        
    // $content .= ...;
}
echo $content;

 

Link to comment
Share on other sites

@abdus You were right, I misplaced the $content result before the end of the loop. I also noticed that I inserted the <ul> inside the list which was breaking the order, so I moved it out of the equation and it worked fine.

The only thing to look at is that if I miss to add a value of the nutritions it shows an empy <li></li> and that messes up the style. 

I tried to check if the result is null/empty and if not to echo $content:

if (!empty($item)){ 
echo $content;
}

But It still shows the empty values ... 

Link to comment
Share on other sites

Just now, MilenKo said:

@abdus You were right, I misplaced the $content result before the end of the loop. I also noticed that I inserted the <ul> inside the list which was breaking the order, so I moved it out of the equation and it worked fine.

The only thing to look at is that if I miss to add a value of the nutritions it shows an empy <li></li> and that messes up the style. 

I tried to check if the result is null/empty and if not to echo $content:

if (!empty($item)){ 
echo $content;
}

But It still shows the empty values ... 

You should check using

if(!empty($item->value)) {
	// ...
}

Because, $item is a WireData object, it can't be empty. But, if value is not populated, $item->value will be empty, and you can check that.

Link to comment
Share on other sites

@abdus for some reason even checking for $item->value being empty did not stop the loop to show the empty <li></li>:

Spoiler

	<ul>	
	<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
	$nutrition = new WireArray();

		// regex pattern to extract values
		$pattern =
		'/^' . // beginning of each line
		'\s*' . // may start with some whitespace
		'(?P<name>[^:]+)' . // name (first part until colon)
		'\s*:\s*' . // a colon that may be surrounded by whitespaces
		'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
		'\s*' .  // after the number there may be some whitespace
		'(?P<unit>\w+)'. // unit (one or more letters)
		'\s*$/'; // there might be some whitespace before the end
		
		foreach (explode("\n", $text) as $line) {
			preg_match($pattern, $line, $parts);
		  
			$nutrition->add((new WireData())->setArray([
				'name' => $parts['name'],
				'amount' => floatval($parts['amount']),
				'unit' => $parts['unit']
			]));
		}
		
		$order = [
			'Calories',
			'Fats',
			'Saturated',
			'Salt',
			'Carbs',
			'Fibre',
			'Sugar',
			'Proteins',
		];

		$markup = '';
		foreach ($order as $o) {
		$item = $nutrition->get("name%=$o");	
		
		$content .= "<li><div class='cs-nutrition-item-description'>$item->name</div><div class='cs-nutrition-item-value'>$item->amount $item->unit</div></li>";
		}
		
		if(!empty($item->name)) {
		echo $content; 
		} ?>
		
	</ul>	

 

We are so close, but still... 

Link to comment
Share on other sites

You should check whether item has  value inside the loop (where you're building $content) and add item to list only if its value is not empty

foreach ($order as $o) {
    $item = $nutrition->get("name%=$o");

    // skip item if its value is not populated
    if(empty($item->value)) continue;
	
    // item has value, add it to the list
    $content .= ...;
}

// now echo the $content
echo $content

 

Link to comment
Share on other sites

OK. Now you nailed it!!!

Here is the final code used to make the nutrition work:

Spoiler

	<ul>	
	<?php $text = $sanitizer->textarea($page->recipe_nutrition); // remove markup from body
	$nutrition = new WireArray();

		// regex pattern to extract values
		$pattern =
		'/^' . // beginning of each line
		'\s*' . // may start with some whitespace
		'(?P<name>[^:]+)' . // name (first part until colon)
		'\s*:\s*' . // a colon that may be surrounded by whitespaces
		'(?P<amount>[\d,\.]+)' . // value (a number that may include , or . like 1,234.56)
		'\s*' .  // after the number there may be some whitespace
		'(?P<unit>\w+)'. // unit (one or more letters)
		'\s*$/'; // there might be some whitespace before the end
		
		foreach (explode("\n", $text) as $line) {
			preg_match($pattern, $line, $parts);
		  
			$nutrition->add((new WireData())->setArray([
				'name' => $parts['name'],
				'amount' => floatval($parts['amount']),
				'unit' => $parts['unit']
			]));
		}
		
		$order = [
			'Calories',
			'Fats',
			'Saturated',
			'Salt',
			'Carbs',
			'Fibre',
			'Sugar',
			'Proteins',
		];

		$markup = '';
		foreach ($order as $o) {
		$item = $nutrition->get("name%=$o");

		// skip item if its value is not populated
		if(empty($item->name)) continue;		
		
		$content .= "<li><div class='cs-nutrition-item-description'>$item->name</div><div class='cs-nutrition-item-value'>$item->amount $item->unit</div></li>";
		}		
		echo $content; 
		?>
	</ul>	

 

I think this would be useful to have for several other locations where a need would be to separate text lines from an textarea field and populate the values if not empty.

One less task to deal with thanks to you @abdus!

Edited by MilenKo
Post was updated to fix the issue of having a few nutritional values only.
Link to comment
Share on other sites

@abdus I just noticed something strange - if I have only 4 values, they don't show at all... I checked up the code to see if it could be an overlapping of divs etc, however it is not showing in the page source either:

<div class="cs-nutrition-list">
	<h4>Nutritions</h4>
	<ul>	
	</ul>	
</div>

If I add more than 4 nutrition rows - it shows them ok.. The happiness did not last long ;)

Link to comment
Share on other sites

For the test I just removed the last 4 of the afore mentioned values list:

Calories: 1000 kCal
Fats: 10 gr
Saturated: 15 gr
Salt: 1 gr

Theoretically it should still find the 4 and list them but...

Link to comment
Share on other sites

@abdus I found the issue and eliminated it.  It appeared that the problem was caused by another check for $nutrition->name before printing the $content

        if(!empty($item->name)) {
        echo $content; 
        } ?>

As soon as I removed the if-check as we are already checking up for empty $item->name before that it worked like a charm! The full code shared above has been edited to provide the fully working sollution.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...